oqimDocs
API referenceConsole

Start here

  • Introduction
  • Getting started

Guides

  • Telegram accounts
  • Recipients
  • Campaigns
  • Scheduling
  • Webhooks
  • AI writing assistant

AI Seller

  • AI Seller
  • Channels
  • Live inbox
  • Past conversations and privacy
  • Business insights

API

  • Authentication
  • API reference
  • SDK
  • Errors
  • Rate limits

Trust and operations

  • Security
  • Compliance
  • Administration
Docs / API reference

Start here

  • Introduction
  • Getting started

Guides

  • Telegram accounts
  • Recipients
  • Campaigns
  • Scheduling
  • Webhooks
  • AI writing assistant

AI Seller

  • AI Seller
  • Channels
  • Live inbox
  • Past conversations and privacy
  • Business insights

API

  • Authentication
  • API reference
  • SDK
  • Errors
  • Rate limits

Trust and operations

  • Security
  • Compliance
  • Administration

API reference

The REST API behind the console: 349 endpoints. Anything the console does, a program can do with an API key, except connecting Telegram accounts and managing people, which a signed-in owner or admin does.

The whole reference is also an OpenAPI 3.1 spec, for client generators, Postman and other tools. GET /api/v1/openapi.json on your Oqim host redirects to it. For TypeScript, use the SDK.

Conventions#

  • Base URL: https://app.example.com/api/v1, your console's host plus /api/v1. Paths below are relative to it.
  • Authenticate with Authorization: Bearer <API key or access token>. See Authentication.
  • Requests and responses are JSON, except uploads (multipart/form-data), attachment downloads and the event stream (text/event-stream). JSON bodies are limited to 2 MB; imports take up to 10 MB and attachments up to 20 MB.
  • Timestamps are RFC 3339 in UTC. Optional fields are null when unset; a few appear only in one response, such as an API key's secret when it's created.
  • Lists take limit (default 50, at most 500) and offset, and return {"data": [...], "total": 1840, "limit": 50, "offset": 0}.
  • Errors share one envelope with a stable code; see Errors. Limits are in Rate limits.
  • Every response carries X-Request-Id, the ID the API logged the request under: the one you sent in that header, or a new one. Quote it when you report a problem.
  • Examples are abbreviated: long objects show their most useful fields.

IDs are strings: a type prefix and a time-sortable suffix, such as cmp_01j9r4c6e8g0j2m4p6r8t0w2y4. IDs from another organization return 404 not_found, the same as IDs that don't exist.

PrefixObjectPrefixObject
org_Organizationusr_User
mem_Membershipinv_Invitation
key_API keyacc_Telegram account
flw_Login flowprx_Proxy
rcp_Recipientlst_List
sup_Suppressionimp_Import
tpl_Templateatt_Attachment
cmp_Campaigncrp_Campaign recipient
job_Send jobevt_Event
whk_Webhookdlv_Webhook delivery
ntf_Notificationabr_Abuse report
aip_AI provider

A first request:

Shell
curl "https://app.example.com/api/v1/campaigns?status=RUNNING&limit=10" \
  -H "Authorization: Bearer $OQIM_API_KEY"

All endpoints#

Jump to a resource, or open an endpoint's example.

Authentication
  • POST/auth/signup
  • POST/auth/login
  • POST/auth/token
  • POST/auth/mfa/verify
  • POST/auth/logout
Profile
  • GET/me
  • PATCH/me
  • POST/me/switch-organization
  • POST/me/support/end
  • POST/me/organizations
  • POST/me/mfa/setup
  • POST/me/mfa/enable
  • POST/me/mfa/disable
Organization
  • GET/organization
  • PATCH/organization
  • POST/organization/accept-policy
  • GET/organization/usage
  • GET/dashboard
  • GET/analytics/messages
Team
  • GET/members
  • PATCH/members/{id}
  • DELETE/members/{id}
  • GET/invitations
  • POST/invitations
  • DELETE/invitations/{id}
  • GET/invitations/lookup
  • POST/invitations/accept
Telegram accounts
  • GET/accounts
  • POST/accounts
  • POST/accounts/login/{flow_id}/code
  • POST/accounts/login/{flow_id}/password
  • GET/accounts/{id}
  • PATCH/accounts/{id}
  • DELETE/accounts/{id}
  • POST/accounts/{id}/pause
  • POST/accounts/{id}/resume
  • POST/accounts/{id}/check
  • POST/accounts/{id}/reauth
  • GET/accounts/{id}/activity
  • POST/accounts/{id}/test-message
  • POST/accounts/{id}/history/import
  • GET/accounts/{id}/history
  • POST/accounts/{id}/history/cancel
Channels: Instagram and Facebook
  • GET/channels/providers
  • POST/channels/instagram/connect
  • GET/channels/instagram/callback
  • POST/channels/facebook/connect
  • GET/channels/facebook/callback
  • GET/channels/accounts
  • GET/channels/accounts/{id}
  • PATCH/channels/accounts/{id}
  • DELETE/channels/accounts/{id}
  • POST/channels/accounts/{id}/check
  • POST/channels/accounts/{id}/history/import
  • GET/channels/accounts/{id}/history
  • POST/channels/accounts/{id}/history/cancel
  • GET/channels/automations
  • POST/channels/automations
  • GET/channels/automations/{id}
  • PATCH/channels/automations/{id}
  • DELETE/channels/automations/{id}
  • GET/channels/accounts/{id}/private-replies
  • GET/channels/accounts/{id}/marketing-topics
  • GET/channels/accounts/{id}/marketing-subscriptions
  • GET/channels/meta/webhook
  • POST/channels/meta/webhook
  • POST/channels/meta/deauthorize
  • POST/channels/meta/data-deletion
  • GET/channels/meta/data-deletion/{code}
Proxies
  • GET/proxies
  • POST/proxies
  • POST/proxies/import
  • POST/proxies/bulk
  • GET/proxies/{id}
  • PATCH/proxies/{id}
  • DELETE/proxies/{id}
  • POST/proxies/{id}/check
  • GET/proxies/{id}/checks
Recipients
  • GET/recipients
  • POST/recipients
  • POST/recipients/import
  • POST/recipients/bulk
  • GET/recipients/tags
  • GET/recipients/{id}
  • PATCH/recipients/{id}
  • DELETE/recipients/{id}
  • POST/recipients/{id}/opt-out
  • GET/recipient-imports
Lists
  • GET/lists
  • POST/lists
  • GET/lists/{id}
  • PATCH/lists/{id}
  • DELETE/lists/{id}
  • POST/lists/{id}/members
  • DELETE/lists/{id}/members
Suppression list
  • GET/suppressions
  • POST/suppressions
  • DELETE/suppressions/{id}
Templates and attachments
  • GET/templates
  • POST/templates
  • PATCH/templates/{id}
  • DELETE/templates/{id}
  • POST/attachments
  • GET/attachments/{id}/content
  • POST/messages/preview
Campaigns
  • GET/campaigns
  • POST/campaigns
  • GET/campaigns/{id}
  • PATCH/campaigns/{id}
  • DELETE/campaigns/{id}
  • POST/campaigns/{id}/preview
  • POST/campaigns/{id}/validate
  • POST/campaigns/{id}/start
  • POST/campaigns/{id}/pause
  • POST/campaigns/{id}/resume
  • POST/campaigns/{id}/cancel
  • POST/campaigns/{id}/stop-repeating
  • POST/campaigns/{id}/duplicate
  • GET/campaigns/{id}/audience-preview
  • GET/campaigns/{id}/statistics
  • GET/campaigns/{id}/recipients
Jobs and events
  • GET/jobs
  • GET/events
  • GET/events/stream
API keys
  • GET/api-keys
  • POST/api-keys
  • POST/api-keys/{id}/rotate
  • DELETE/api-keys/{id}
Webhooks
  • GET/webhooks
  • POST/webhooks
  • GET/webhooks/{id}
  • PATCH/webhooks/{id}
  • DELETE/webhooks/{id}
  • POST/webhooks/{id}/test
  • POST/webhooks/{id}/rotate-secret
  • GET/webhooks/{id}/deliveries
AI Seller: configuration
  • GET/ai/businesses
  • POST/ai/businesses
  • GET/ai/businesses/{id}
  • PATCH/ai/businesses/{id}
  • DELETE/ai/businesses/{id}
  • GET/ai/businesses/{id}/offers
  • POST/ai/businesses/{id}/offers
  • PATCH/ai/offers/{id}
  • DELETE/ai/offers/{id}
  • GET/ai/businesses/{id}/funnels
  • POST/ai/businesses/{id}/funnels
  • GET/ai/funnels/{id}
  • PATCH/ai/funnels/{id}
  • DELETE/ai/funnels/{id}
  • PUT/ai/funnels/{id}/stages
  • GET/ai/businesses/{id}/identities
  • POST/ai/businesses/{id}/identities
  • PATCH/ai/identities/{id}
  • DELETE/ai/identities/{id}
  • GET/ai/communication-profiles
  • POST/ai/communication-profiles
  • PATCH/ai/communication-profiles/{id}
  • DELETE/ai/communication-profiles/{id}
  • POST/ai/communication-profiles/{id}/copy
  • GET/ai/language-profiles
  • POST/ai/language-profiles
  • PATCH/ai/language-profiles/{id}
  • DELETE/ai/language-profiles/{id}
  • POST/ai/language-profiles/{id}/copy
  • GET/ai/businesses/{id}/seller-settings
  • PUT/ai/businesses/{id}/seller-settings
  • GET/ai/businesses/{id}/accounts
  • PUT/ai/businesses/{id}/accounts
AI
  • GET/ai/catalog
  • GET/ai/providers
  • POST/ai/providers
  • PATCH/ai/providers/{id}
  • DELETE/ai/providers/{id}
  • POST/ai/providers/{id}/test
  • GET/ai/providers/{id}/models
  • POST/ai/models
  • POST/ai/compose
AI engine
  • GET/ai/agents
  • GET/ai/agents/{agent}/config
  • PATCH/ai/agents/{agent}/config
  • DELETE/ai/agents/{agent}/config
  • GET/ai/models
  • GET/ai/prompts
  • POST/ai/prompts
  • GET/ai/prompts/{id}/versions
  • POST/ai/prompts/{id}/versions
  • POST/ai/prompts/{id}/versions/{v}/activate
  • GET/ai/executions
  • GET/ai/executions/{id}
  • GET/ai/costs
  • GET/ai/budgets
  • PUT/ai/budgets
AI Seller: conversations
  • GET/ai/conversations
  • GET/ai/conversations/{id}
  • POST/ai/conversations/{id}/reply
  • POST/ai/conversations/{id}/takeover
  • POST/ai/conversations/{id}/resume
  • POST/ai/conversations/{id}/read
  • POST/ai/conversations/{id}/platform-read
  • GET/ai/conversations/{id}/attachments/{attachment_id}
  • POST/ai/conversations/{id}/uploads
  • PUT/ai/conversations/{id}/personal
  • GET/ai/conversations/{id}/insights
  • POST/ai/conversations/{id}/analyze
  • POST/ai/conversations/simulate
  • POST/ai/playground/run
  • GET/ai/handoffs
  • PATCH/ai/handoffs/{id}
  • POST/ai/conversations/{id}/drafts/{draft}/approve
  • POST/ai/conversations/{id}/drafts/{draft}/discard
  • GET/ai/leads
  • GET/ai/sales-pipeline
  • GET/ai/analytics
  • GET/ai/performance
AI Seller: knowledge and memory
  • GET/ai/knowledge/sources
  • POST/ai/knowledge/sources
  • GET/ai/knowledge/sources/{id}
  • PATCH/ai/knowledge/sources/{id}
  • DELETE/ai/knowledge/sources/{id}
  • POST/ai/knowledge/sources/{id}/reindex
  • GET/ai/knowledge/search
  • GET/ai/memory
  • DELETE/ai/memory/{id}
  • POST/ai/research
  • GET/ai/research
  • GET/ai/research/{id}
  • POST/ai/research/{id}/save-to-knowledge
AI Seller: decisions
  • GET/ai/jev
  • PUT/ai/jev
  • POST/ai/jev/test
  • GET/ai/jev/decisions/{id}
AI Seller: quality
  • POST/ai/feedback
  • GET/ai/feedback
  • GET/ai/feedback/{id}
  • POST/ai/feedback/{id}/review
  • POST/ai/eval-datasets
  • GET/ai/eval-datasets
  • GET/ai/eval-datasets/{id}
  • DELETE/ai/eval-datasets/{id}
  • POST/ai/eval-datasets/{id}/items
  • GET/ai/eval-datasets/{id}/items
  • POST/ai/evaluations
  • GET/ai/evaluations
  • GET/ai/evaluations/{id}
  • POST/ai/evaluations/{id}/run
  • GET/ai/evaluations/{id}/results
  • POST/ai/experiments
  • GET/ai/experiments
  • GET/ai/experiments/{id}
  • POST/ai/experiments/{id}/variants
  • POST/ai/experiments/{id}/winner
AI Seller: calendar
  • GET/ai/calendar
  • POST/ai/calendar/connect
  • GET/ai/calendar/oauth/callback
  • POST/ai/calendar/disconnect
  • GET/ai/calendar/actions
  • POST/ai/calendar/actions/{id}/confirm
  • POST/ai/calendar/actions/{id}/reject
AI Seller: case analysis and insights
  • GET/ai/businesses/{id}/cases
  • POST/ai/businesses/{id}/cases
  • POST/ai/businesses/{id}/cases/generate
  • GET/ai/businesses/{id}/cases/generation
  • PATCH/ai/cases/{id}
  • DELETE/ai/cases/{id}
  • GET/ai/conversations/{id}/analysis
  • POST/ai/conversations/{id}/analysis/refresh
  • POST/ai/conversations/{id}/analysis/explain
  • PUT/ai/conversations/{id}/cases/{dimension}
  • GET/ai/insights/boards/{board}
  • POST/ai/insights/boards/{board}/narrative
  • GET/ai/insights/boards/{board}/cases/{case_id}/conversations
  • GET/ai/insights/backfill
  • POST/ai/insights/backfill
  • POST/ai/insights/backfill/cancel
Social content
  • POST/channels/youtube/accounts
  • POST/channels/youtube/connect
  • GET/channels/youtube/callback
  • GET/ai/social/accounts/{id}/sync
  • POST/ai/social/accounts/{id}/sync
  • GET/ai/social/boards/{board}
  • POST/ai/social/boards/{board}/narrative
  • GET/ai/social/posts
  • GET/ai/social/posts/{id}
  • GET/ai/social/comments
Audit log and notifications
  • GET/audit-logs
  • GET/organization/support-sessions
  • GET/organization/support-sessions/{id}/accesses
  • GET/support/enter
  • GET/notifications
  • POST/notifications/read
Public
  • GET/public/opt-out/{token}
  • POST/public/opt-out/{token}
  • GET/public/inbox/attachments/{attachment_id}
  • GET/public/config
  • GET/openapi.json
Administration API
  • GET/admin/overview
  • GET/admin/organizations
  • POST/admin/organizations
  • GET/admin/organizations/{id}
  • PATCH/admin/organizations/{id}
  • POST/admin/organizations/{id}/suspend
  • POST/admin/organizations/{id}/reactivate
  • GET/admin/users
  • POST/admin/users/{id}/disable
  • POST/admin/users/{id}/enable
  • GET/admin/accounts
  • POST/admin/accounts/{id}/disable
  • POST/admin/accounts/{id}/enable
  • GET/admin/proxies
  • GET/admin/proxies/{id}
  • POST/admin/proxies/{id}/check
  • POST/admin/proxies/{id}/disable
  • POST/admin/proxies/{id}/enable
  • GET/admin/campaigns
  • GET/admin/campaigns/{id}
  • POST/admin/campaigns/{id}/pause
  • POST/admin/campaigns/{id}/resume
  • POST/admin/campaigns/{id}/cancel
  • GET/admin/workers
  • GET/admin/queues
  • POST/admin/queues/{name}/pause
  • POST/admin/queues/{name}/resume
  • GET/admin/queues/{name}/tasks
  • POST/admin/queues/{name}/tasks/{task}/retry
  • POST/admin/queues/{name}/tasks/{task}/drop
  • POST/admin/queues/{name}/retry-archived
  • POST/admin/queues/{name}/drop-archived
  • GET/admin/system
  • POST/admin/organizations/{id}/support-sessions
  • GET/admin/support-sessions
  • POST/admin/support-sessions/{id}/end
  • GET/admin/support-sessions/{id}/accesses
  • GET/admin/organizations/{id}/limits
  • PUT/admin/organizations/{id}/limits
  • GET/admin/organizations/{id}/ai/budgets
  • PUT/admin/organizations/{id}/ai/budgets
  • GET/admin/organizations/{id}/ai/costs
  • GET/admin/abuse-reports
  • POST/admin/abuse-reports
  • PATCH/admin/abuse-reports/{id}
  • GET/admin/audit-logs
  • GET/admin/settings
  • PUT/admin/settings
  • GET/admin/billing
  • GET/admin/ai/overview
  • GET/admin/ai/models
  • POST/admin/ai/models
  • PATCH/admin/ai/models/{id}

Authentication#

Sign-up, sign-in and the API-key token exchange. These public endpoints are limited to 30 requests per minute per IP address.

POST/auth/signupPublic

Create a user and their organization, then start a console session. Refused with signups_closed when sign-ups are off.

Example
Request body
{
  "name": "Dilnoza Yusupova",
  "email": "dilnoza@silkroad.example",
  "password": "at-least-10-characters",
  "organization_name": "Silk Road Events",
  "timezone": "Asia/Tashkent"
}
Response · 201 Created
{
  "kind": "user",
  "user": {
    "id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "email": "dilnoza@silkroad.example",
    "name": "Dilnoza Yusupova",
    "is_platform_admin": false,
    "status": "ACTIVE",
    "mfa_enabled": true,
    "last_login_at": "2026-09-26T07:58:10Z",
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-20T12:00:00Z"
  },
  "organization": {
    "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "name": "Silk Road Events",
    "slug": "silk-road-events",
    "status": "ACTIVE",
    "plan": "PRO",
    "aup_version": "2026-09"
  },
  "role": "OWNER",
  "permissions": [
    "org:read",
    "org:manage",
    "members:read",
    "members:manage",
    "accounts:read",
    "accounts:manage",
    "proxies:read",
    "proxies:manage",
    "recipients:read",
    "recipients:manage",
    "campaigns:read",
    "campaigns:manage",
    "campaigns:execute",
    "analytics:read",
    "audit:read",
    "developers:manage",
    "webhooks:read",
    "webhooks:manage",
    "billing:read"
  ],
  "organizations": [
    {
      "role": "OWNER",
      "organization": {
        "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
        "name": "Silk Road Events",
        "plan": "PRO"
      }
    }
  ],
  "mfa_verified": true,
  "policy_version": "2026-09"
}
POST/auth/loginPublic

Start a console session and set the oqim_session cookie. With two-factor authentication on, the session is limited until POST /auth/mfa/verify.

Example
Request body
{
  "email": "dilnoza@silkroad.example",
  "password": "at-least-10-characters"
}
Response · 200 OK
{
  "mfa_required": true
}
POST/auth/tokenPublic

Exchange an API key for a one-hour access token (oqat_…). Use it as a bearer token exactly like the key.

Example
Request body
{
  "grant_type": "api_key",
  "api_key": "oqk_n4v8t2kq7wzc_yR3fT9qLm2Vx8KpZc6WnB4sHd1JgQ7eU0aXoN5tYiEw"
}
Response · 200 OK
{
  "access_token": "oqat_eyJrIjoia2V5XzAxajlyOGExYzNlNWc3ajltMXAzcjV0N3c5IiwibyI6Im9yZ18wMWo5cjJtNmsi.Hq2vX8bN3mK5pL7rT9wY1zA4cE6gJ0sU",
  "token_type": "Bearer",
  "expires_in": 3600,
  "expires_at": "2026-09-26T10:30:00Z"
}
POST/auth/mfa/verifySigned in

Complete sign-in with a 6-digit TOTP code. One 30-second step of clock drift is accepted.

Example
Request body
{
  "code": "492039"
}
Response · 200 OK
{
  "kind": "user",
  "user": {
    "id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "email": "dilnoza@silkroad.example",
    "name": "Dilnoza Yusupova",
    "is_platform_admin": false,
    "status": "ACTIVE",
    "mfa_enabled": true,
    "last_login_at": "2026-09-26T07:58:10Z",
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-20T12:00:00Z"
  },
  "organization": {
    "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "name": "Silk Road Events",
    "slug": "silk-road-events",
    "status": "ACTIVE",
    "plan": "PRO",
    "aup_version": "2026-09"
  },
  "role": "OWNER",
  "permissions": [
    "org:read",
    "org:manage",
    "members:read",
    "members:manage",
    "accounts:read",
    "accounts:manage",
    "proxies:read",
    "proxies:manage",
    "recipients:read",
    "recipients:manage",
    "campaigns:read",
    "campaigns:manage",
    "campaigns:execute",
    "analytics:read",
    "audit:read",
    "developers:manage",
    "webhooks:read",
    "webhooks:manage",
    "billing:read"
  ],
  "organizations": [
    {
      "role": "OWNER",
      "organization": {
        "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
        "name": "Silk Road Events",
        "plan": "PRO"
      }
    }
  ],
  "mfa_verified": true,
  "policy_version": "2026-09"
}
POST/auth/logoutSigned in

End the console session and clear the cookie.

Example

Response · 204 No Content, no body

Profile#

The signed-in user or API key. The two-factor endpoints are for people; API keys get not_applicable.

GET/meSigned in

Who is calling: the user or API key, the active organization, role, permissions and the current policy version.

Example
Response · 200 OK
{
  "kind": "user",
  "user": {
    "id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "email": "dilnoza@silkroad.example",
    "name": "Dilnoza Yusupova",
    "is_platform_admin": false,
    "status": "ACTIVE",
    "mfa_enabled": true,
    "last_login_at": "2026-09-26T07:58:10Z",
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-20T12:00:00Z"
  },
  "organization": {
    "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "name": "Silk Road Events",
    "slug": "silk-road-events",
    "status": "ACTIVE",
    "plan": "PRO",
    "aup_version": "2026-09"
  },
  "role": "OWNER",
  "permissions": [
    "org:read",
    "org:manage",
    "members:read",
    "members:manage",
    "accounts:read",
    "accounts:manage",
    "proxies:read",
    "proxies:manage",
    "recipients:read",
    "recipients:manage",
    "campaigns:read",
    "campaigns:manage",
    "campaigns:execute",
    "analytics:read",
    "audit:read",
    "developers:manage",
    "webhooks:read",
    "webhooks:manage",
    "billing:read"
  ],
  "organizations": [
    {
      "role": "OWNER",
      "organization": {
        "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
        "name": "Silk Road Events",
        "plan": "PRO"
      }
    }
  ],
  "mfa_verified": true,
  "policy_version": "2026-09"
}
PATCH/meSigned in

Change your name or password. A new password needs the current one and at least 10 characters.

Example
Request body
{
  "name": "Dilnoza Yusupova",
  "current_password": "…",
  "new_password": "a-new-long-password"
}
Response · 200 OK
{
  "kind": "user",
  "user": {
    "id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "email": "dilnoza@silkroad.example",
    "name": "Dilnoza Yusupova",
    "is_platform_admin": false,
    "status": "ACTIVE",
    "mfa_enabled": true,
    "last_login_at": "2026-09-26T07:58:10Z",
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-20T12:00:00Z"
  },
  "organization": {
    "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "name": "Silk Road Events",
    "slug": "silk-road-events",
    "status": "ACTIVE",
    "plan": "PRO",
    "aup_version": "2026-09"
  },
  "role": "OWNER",
  "permissions": [
    "org:read",
    "org:manage",
    "members:read",
    "members:manage",
    "accounts:read",
    "accounts:manage",
    "proxies:read",
    "proxies:manage",
    "recipients:read",
    "recipients:manage",
    "campaigns:read",
    "campaigns:manage",
    "campaigns:execute",
    "analytics:read",
    "audit:read",
    "developers:manage",
    "webhooks:read",
    "webhooks:manage",
    "billing:read"
  ],
  "organizations": [
    {
      "role": "OWNER",
      "organization": {
        "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
        "name": "Silk Road Events",
        "plan": "PRO"
      }
    }
  ],
  "mfa_verified": true,
  "policy_version": "2026-09"
}
POST/me/switch-organizationSigned in

Make another of your organizations the session's active one.

Example
Request body
{
  "organization_id": "org_01j9s0a2c4e6g8j0m2p4r6t8w0"
}
Response · 200 OK
{
  "kind": "user",
  "user": {
    "id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "email": "dilnoza@silkroad.example",
    "name": "Dilnoza Yusupova",
    "is_platform_admin": false,
    "status": "ACTIVE",
    "mfa_enabled": true,
    "last_login_at": "2026-09-26T07:58:10Z",
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-20T12:00:00Z"
  },
  "organization": {
    "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "name": "Silk Road Events",
    "slug": "silk-road-events",
    "status": "ACTIVE",
    "plan": "PRO",
    "aup_version": "2026-09"
  },
  "role": "OWNER",
  "permissions": [
    "org:read",
    "org:manage",
    "members:read",
    "members:manage",
    "accounts:read",
    "accounts:manage",
    "proxies:read",
    "proxies:manage",
    "recipients:read",
    "recipients:manage",
    "campaigns:read",
    "campaigns:manage",
    "campaigns:execute",
    "analytics:read",
    "audit:read",
    "developers:manage",
    "webhooks:read",
    "webhooks:manage",
    "billing:read"
  ],
  "organizations": [
    {
      "role": "OWNER",
      "organization": {
        "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
        "name": "Silk Road Events",
        "plan": "PRO"
      }
    }
  ],
  "mfa_verified": true,
  "policy_version": "2026-09"
}
POST/me/support/endSigned in

End your own support session: the record closes, the organization is told, and the cookie is cleared. 409 not_in_support_session for any other session. Signing out does the same. While in a support session this and sign-out are the only writes allowed; everything else is 403 support_read_only.

Example

Response · 204 No Content, no body

POST/me/organizationsSigned in

Create an organization with you as its owner and switch to it.

Example
Request body
{
  "name": "Silk Road Tours",
  "timezone": "Asia/Samarkand"
}
Response · 201 Created
{
  "kind": "user",
  "user": {
    "id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "email": "dilnoza@silkroad.example",
    "name": "Dilnoza Yusupova",
    "is_platform_admin": false,
    "status": "ACTIVE",
    "mfa_enabled": true,
    "last_login_at": "2026-09-26T07:58:10Z",
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-20T12:00:00Z"
  },
  "organization": {
    "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "name": "Silk Road Events",
    "slug": "silk-road-events",
    "status": "ACTIVE",
    "plan": "PRO",
    "aup_version": "2026-09"
  },
  "role": "OWNER",
  "permissions": [
    "org:read",
    "org:manage",
    "members:read",
    "members:manage",
    "accounts:read",
    "accounts:manage",
    "proxies:read",
    "proxies:manage",
    "recipients:read",
    "recipients:manage",
    "campaigns:read",
    "campaigns:manage",
    "campaigns:execute",
    "analytics:read",
    "audit:read",
    "developers:manage",
    "webhooks:read",
    "webhooks:manage",
    "billing:read"
  ],
  "organizations": [
    {
      "role": "OWNER",
      "organization": {
        "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
        "name": "Silk Road Events",
        "plan": "PRO"
      }
    }
  ],
  "mfa_verified": true,
  "policy_version": "2026-09"
}
POST/me/mfa/setupSigned in

Start two-factor enrollment. Returns the TOTP secret and an otpauth:// URL for a QR code; nothing changes until you enable it.

Example
Response · 200 OK
{
  "secret": "JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP",
  "otpauth_url": "otpauth://totp/Oqim:dilnoza@silkroad.example?algorithm=SHA1&digits=6&issuer=Oqim&period=30&secret=JBSWY3DPEHPK3PXPJBSWY3DPEHPK3PXP"
}
POST/me/mfa/enableSigned in

Turn two-factor authentication on by confirming a code from the authenticator. The current session counts as verified.

Example
Request body
{
  "code": "492039"
}
Response · 200 OK
{
  "kind": "user",
  "user": {
    "id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "email": "dilnoza@silkroad.example",
    "name": "Dilnoza Yusupova",
    "is_platform_admin": false,
    "status": "ACTIVE",
    "mfa_enabled": true,
    "last_login_at": "2026-09-26T07:58:10Z",
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-20T12:00:00Z"
  },
  "organization": {
    "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "name": "Silk Road Events",
    "slug": "silk-road-events",
    "status": "ACTIVE",
    "plan": "PRO",
    "aup_version": "2026-09"
  },
  "role": "OWNER",
  "permissions": [
    "org:read",
    "org:manage",
    "members:read",
    "members:manage",
    "accounts:read",
    "accounts:manage",
    "proxies:read",
    "proxies:manage",
    "recipients:read",
    "recipients:manage",
    "campaigns:read",
    "campaigns:manage",
    "campaigns:execute",
    "analytics:read",
    "audit:read",
    "developers:manage",
    "webhooks:read",
    "webhooks:manage",
    "billing:read"
  ],
  "organizations": [
    {
      "role": "OWNER",
      "organization": {
        "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
        "name": "Silk Road Events",
        "plan": "PRO"
      }
    }
  ],
  "mfa_verified": true,
  "policy_version": "2026-09"
}
POST/me/mfa/disableSigned in

Turn two-factor authentication off. Needs a current code. Platform administrators can't open the admin console without it.

Example
Request body
{
  "code": "118305"
}
Response · 200 OK
{
  "kind": "user",
  "user": {
    "id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "email": "dilnoza@silkroad.example",
    "name": "Dilnoza Yusupova",
    "is_platform_admin": false,
    "status": "ACTIVE",
    "mfa_enabled": true,
    "last_login_at": "2026-09-26T07:58:10Z",
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-20T12:00:00Z"
  },
  "organization": {
    "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "name": "Silk Road Events",
    "slug": "silk-road-events",
    "status": "ACTIVE",
    "plan": "PRO",
    "aup_version": "2026-09"
  },
  "role": "OWNER",
  "permissions": [
    "org:read",
    "org:manage",
    "members:read",
    "members:manage",
    "accounts:read",
    "accounts:manage",
    "proxies:read",
    "proxies:manage",
    "recipients:read",
    "recipients:manage",
    "campaigns:read",
    "campaigns:manage",
    "campaigns:execute",
    "analytics:read",
    "audit:read",
    "developers:manage",
    "webhooks:read",
    "webhooks:manage",
    "billing:read"
  ],
  "organizations": [
    {
      "role": "OWNER",
      "organization": {
        "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
        "name": "Silk Road Events",
        "plan": "PRO"
      }
    }
  ],
  "mfa_verified": true,
  "policy_version": "2026-09"
}

Organization#

The active organization, its policy acceptance, usage and dashboard data.

GET/organizationorg:read

The active organization.

Example
Response · 200 OK
{
  "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Silk Road Events",
  "slug": "silk-road-events",
  "status": "ACTIVE",
  "plan": "PRO",
  "timezone": "Asia/Tashkent",
  "suspended_reason": null,
  "suspended_at": null,
  "aup_version": "2026-09",
  "aup_accepted_at": "2026-09-02T08:12:44Z",
  "aup_accepted_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "conversation_intelligence_enabled": true,
  "created_at": "2026-09-01T10:00:00Z",
  "updated_at": "2026-09-02T08:12:44Z"
}
PATCH/organizationorg:manage

Rename the organization, change its default time zone, or turn its Conversation Intelligence (summaries, memory and the case analysis of conversations) off and on with conversation_intelligence_enabled.

Example
Request body
{
  "name": "Silk Road Events",
  "timezone": "Asia/Tashkent",
  "conversation_intelligence_enabled": true
}
Response · 200 OK
{
  "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Silk Road Events",
  "slug": "silk-road-events",
  "status": "ACTIVE",
  "plan": "PRO",
  "timezone": "Asia/Tashkent",
  "suspended_reason": null,
  "suspended_at": null,
  "aup_version": "2026-09",
  "aup_accepted_at": "2026-09-02T08:12:44Z",
  "aup_accepted_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "conversation_intelligence_enabled": true,
  "created_at": "2026-09-01T10:00:00Z",
  "updated_at": "2026-09-02T08:12:44Z"
}
POST/organization/accept-policyorg:manage

Accept the acceptable-use policy. version must be the current one (policy_version in GET /me); anything else is a 422. Required before connecting accounts, launching or resuming.

Example
Request body
{
  "version": "2026-09"
}
Response · 200 OK
{
  "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Silk Road Events",
  "slug": "silk-road-events",
  "status": "ACTIVE",
  "plan": "PRO",
  "timezone": "Asia/Tashkent",
  "suspended_reason": null,
  "suspended_at": null,
  "aup_version": "2026-09",
  "aup_accepted_at": "2026-09-02T08:12:44Z",
  "aup_accepted_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "conversation_intelligence_enabled": true,
  "created_at": "2026-09-01T10:00:00Z",
  "updated_at": "2026-09-02T08:12:44Z"
}
GET/organization/usagebilling:read

The plan's limits, current usage, and 30 days of metered usage (messages sent, API requests, and AI requests and tokens when used) per UTC day. A limit of 0 means unlimited.

Example
Response · 200 OK
{
  "plan": {
    "plan": "PRO",
    "accounts": 10,
    "active_campaigns": 10,
    "recipients": 50000,
    "team_members": 5,
    "webhooks": 5,
    "api_access": true,
    "analytics": "basic",
    "price_monthly_usd": 49
  },
  "current": {
    "accounts": 3,
    "active_campaigns": 1,
    "recipients": 4210,
    "members": 4,
    "webhooks": 1
  },
  "daily": [
    { "date": "2026-09-25", "messages_sent": 1204, "api_requests": 311 }
  ]
}
GET/dashboardanalytics:read

Overview numbers, running campaigns, account health, queue state and 14 days of delivery outcomes.

Example
Response · 200 OK
{
  "kpis": {
    "telegram_accounts": 3,
    "active_campaigns": 1,
    "scheduled_campaigns": 2,
    "recipients": 4210,
    "messages_sent": 12840,
    "messages_failed": 96,
    "restricted_accounts": 0
  },
  "attention": [],
  "running": [
    {
      "id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
      "name": "Expo reminder, day before",
      "status": "RUNNING",
      "total_recipients": 1840,
      "delivered_count": 612
    }
  ],
  "series": [
    {
      "date": "2026-09-25",
      "delivered": 1204,
      "failed": 9,
      "unavailable": 31
    }
  ],
  "account_health": { "ACTIVE": 3 },
  "queue": {
    "pending": 1212,
    "queued": 2,
    "sending": 1,
    "delivered_last_hour": 468
  }
}
GET/analytics/messagesanalytics:read

Delivery outcomes over the last days (1 to 365, default 30) in the organization's time zone: per day, per account and per campaign.

Query: days

Example
Response · 200 OK
{
  "days": 30,
  "timezone": "Asia/Tashkent",
  "series": [
    {
      "date": "2026-09-25",
      "delivered": 1204,
      "failed": 9,
      "unavailable": 31
    }
  ],
  "by_account": [
    {
      "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "display_name": "Silk Road Support",
      "delivered": 3180,
      "failed": 12,
      "unavailable": 41
    }
  ],
  "by_campaign": [
    {
      "campaign_id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
      "name": "Expo reminder, day before",
      "delivered": 612,
      "failed": 4,
      "unavailable": 9
    }
  ],
  "totals": {
    "delivered": 12840,
    "failed": 96,
    "unavailable": 355,
    "attempted": 13291,
    "success_rate": 0.966
  }
}

Team#

Members, their roles and invitations. Owners can only be changed by owners.

GET/membersmembers:read

Members of the organization with their role and sign-in details.

Example
Response · 200 OK
{
  "data": [
    {
      "id": "mem_01j9rk9p1s3v5x7z9b1d3f5h7k",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "user_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "role": "OWNER",
      "created_at": "2026-09-01T10:00:00Z",
      "user_name": "Dilnoza Yusupova",
      "user_email": "dilnoza@silkroad.example",
      "last_login_at": "2026-09-26T07:58:10Z",
      "mfa_enabled": true
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
PATCH/members/{id}members:manage

Change a member's role: OWNER, ADMIN, MANAGER, OPERATOR or VIEWER. Only owners grant or remove the owner role, and the last owner can't be demoted (409 last_owner).

Example
Request body
{
  "role": "MANAGER"
}
Response · 200 OK
{
  "id": "mem_01j9rk9p1s3v5x7z9b1d3f5h7k",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "user_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "role": "MANAGER",
  "created_at": "2026-09-01T10:00:00Z",
  "user_name": "Dilnoza Yusupova",
  "user_email": "dilnoza@silkroad.example",
  "last_login_at": "2026-09-26T07:58:10Z",
  "mfa_enabled": true
}
DELETE/members/{id}members:manage

Remove a member from the organization. Their user remains. The last owner can't be removed.

Example

Response · 204 No Content, no body

GET/invitationsmembers:read

Pending and past invitations.

Example
Response · 200 OK
{
  "data": [
    {
      "id": "inv_01j9rj6m8p0r2t4w6y8a0c2e4g",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "email": "timur@silkroad.example",
      "role": "OPERATOR",
      "invited_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "expires_at": "2026-10-03T09:30:00Z",
      "accepted_at": null,
      "revoked_at": null,
      "created_at": "2026-09-26T09:30:00Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/invitationsmembers:manage

Invite someone by email. Oqim doesn't send email: share invite_url, which appears only in this response. Invitations expire after 7 days.

Example
Request body
{
  "email": "timur@silkroad.example",
  "role": "OPERATOR"
}
Response · 201 Created
{
  "id": "inv_01j9rj6m8p0r2t4w6y8a0c2e4g",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "email": "timur@silkroad.example",
  "role": "OPERATOR",
  "invited_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "expires_at": "2026-10-03T09:30:00Z",
  "accepted_at": null,
  "revoked_at": null,
  "created_at": "2026-09-26T09:30:00Z",
  "invite_url": "https://app.example.com/invite/Zk3u9QxP2mVb7Ld0sWc4Rt8nYe1Ha6Jg5Uo2Fi9Kp0"
}
DELETE/invitations/{id}members:manage

Revoke an invitation. One that was already accepted can't be revoked (409 invitation_accepted); remove the member instead.

Example

Response · 204 No Content, no body

GET/invitations/lookupPublic

Describe an invitation from its token before accepting it. Unknown tokens are 404 invalid_invitation; used, revoked or expired ones are 410.

Query: token

Example
Response · 200 OK
{
  "organization_name": "Silk Road Events",
  "email": "timur@silkroad.example",
  "role": "OPERATOR",
  "expires_at": "2026-10-03T09:30:00Z",
  "existing_user": false
}
POST/invitations/acceptPublic

Accept an invitation and start a session. New users set their name and password here; existing users must be signed in as the invited email.

Example
Request body
{
  "token": "Zk3u9QxP2mVb7Ld0sWc4Rt8nYe1Ha6Jg5Uo2Fi9Kp0",
  "name": "Timur Rakhimov",
  "password": "at-least-10-characters"
}
Response · 200 OK
{
  "kind": "user",
  "user": {
    "id": "usr_01j9s2c4e6g8j0m2p4r6t8w0y2",
    "email": "timur@silkroad.example",
    "name": "Timur Rakhimov",
    "is_platform_admin": false,
    "status": "ACTIVE",
    "mfa_enabled": false,
    "last_login_at": "2026-09-26T07:58:10Z",
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-20T12:00:00Z"
  },
  "organization": {
    "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "name": "Silk Road Events",
    "slug": "silk-road-events",
    "status": "ACTIVE",
    "plan": "PRO",
    "aup_version": "2026-09"
  },
  "role": "OPERATOR",
  "permissions": [
    "org:read",
    "org:manage",
    "members:read",
    "members:manage",
    "accounts:read",
    "accounts:manage",
    "proxies:read",
    "proxies:manage",
    "recipients:read",
    "recipients:manage",
    "campaigns:read",
    "campaigns:manage",
    "campaigns:execute",
    "analytics:read",
    "audit:read",
    "developers:manage",
    "webhooks:read",
    "webhooks:manage",
    "billing:read"
  ],
  "organizations": [
    {
      "role": "OWNER",
      "organization": {
        "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
        "name": "Silk Road Events",
        "plan": "PRO"
      }
    }
  ],
  "mfa_verified": true,
  "policy_version": "2026-09"
}

Telegram accounts#

Accounts your organization owns. Connecting is a three-step sign-in that a person completes; API keys can read accounts but not connect them. See Telegram accounts for states and pacing.

GET/accountsaccounts:read

Connected accounts.

Query: status, q, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "telegram_user_id": 5712093344,
      "display_name": "Silk Road Support",
      "username": "silkroad_support",
      "phone_masked": "+998 •• ••• 45 17",
      "status": "ACTIVE",
      "status_reason": null,
      "status_changed_at": "2026-09-03T06:41:12Z",
      "proxy_id": null,
      "daily_limit": 150,
      "min_interval_seconds": 8,
      "cooldown_until": null,
      "next_send_at": "2026-09-26T09:30:08Z",
      "sent_today": 42,
      "sent_today_date": "2026-09-26T00:00:00Z",
      "total_sent": 3180,
      "total_failed": 12,
      "consecutive_errors": 0,
      "last_seen_at": "2026-09-26T09:29:51Z",
      "last_error": null,
      "inbound_enabled": false,
      "created_at": "2026-09-03T06:40:00Z",
      "updated_at": "2026-09-26T09:30:00Z",
      "proxy_name": null,
      "active_campaigns": 1
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/accountsaccounts:manage

Start signing in to an account: Telegram sends a login code, usually to the Telegram app. The flow lasts 15 minutes. Needs an accepted acceptable-use policy. A proxy_id of a disabled proxy is a 422.

Example
Request body
{
  "phone": "+998901234517",
  "proxy_id": null
}
Response · 201 Created
{
  "state": {
    "flow_id": "flw_01j9r9k3m5p7r9t1v3x5z7b9d1",
    "step": "CODE_SENT",
    "phone_masked": "+998 •• ••• 45 17",
    "expires_at": "2026-09-26T09:40:00Z",
    "code_type": "app"
  },
  "account": null
}
POST/accounts/login/{flow_id}/codeaccounts:manage

Submit the login code. The step becomes PASSWORD_REQUIRED when two-step verification is on, otherwise COMPLETED with the account. A wrong code is 422 phone_code_invalid; a flow allows 5 attempts.

Example
Request body
{
  "code": "54210"
}
Response · 200 OK
{
  "state": {
    "flow_id": "flw_01j9r9k3m5p7r9t1v3x5z7b9d1",
    "step": "PASSWORD_REQUIRED",
    "phone_masked": "+998 •• ••• 45 17",
    "expires_at": "2026-09-26T09:40:00Z",
    "password_hint": "office safe"
  },
  "account": null
}
POST/accounts/login/{flow_id}/passwordaccounts:manage

Submit the two-step verification password to finish signing in. A wrong password is 422 password_hash_invalid.

Example
Request body
{
  "password": "…"
}
Response · 200 OK
{
  "state": {
    "flow_id": "flw_01j9r9k3m5p7r9t1v3x5z7b9d1",
    "step": "COMPLETED",
    "phone_masked": "+998 •• ••• 45 17",
    "expires_at": "2026-09-26T09:40:00Z",
    "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9"
  },
  "account": {
    "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "telegram_user_id": 5712093344,
    "display_name": "Silk Road Support",
    "username": "silkroad_support",
    "phone_masked": "+998 •• ••• 45 17",
    "status": "ACTIVE",
    "status_reason": null,
    "status_changed_at": "2026-09-03T06:41:12Z",
    "proxy_id": null,
    "daily_limit": 150,
    "min_interval_seconds": 8,
    "cooldown_until": null,
    "next_send_at": "2026-09-26T09:30:08Z",
    "sent_today": 42,
    "sent_today_date": "2026-09-26T00:00:00Z",
    "total_sent": 3180,
    "total_failed": 12,
    "consecutive_errors": 0,
    "last_seen_at": "2026-09-26T09:29:51Z",
    "last_error": null,
    "inbound_enabled": false,
    "created_at": "2026-09-03T06:40:00Z",
    "updated_at": "2026-09-26T09:30:00Z",
    "proxy_name": null,
    "active_campaigns": 1
  }
}
GET/accounts/{id}accounts:read

One account.

Example
Response · 200 OK
{
  "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "telegram_user_id": 5712093344,
  "display_name": "Silk Road Support",
  "username": "silkroad_support",
  "phone_masked": "+998 •• ••• 45 17",
  "status": "ACTIVE",
  "status_reason": null,
  "status_changed_at": "2026-09-03T06:41:12Z",
  "proxy_id": null,
  "daily_limit": 150,
  "min_interval_seconds": 8,
  "cooldown_until": null,
  "next_send_at": "2026-09-26T09:30:08Z",
  "sent_today": 42,
  "sent_today_date": "2026-09-26T00:00:00Z",
  "total_sent": 3180,
  "total_failed": 12,
  "consecutive_errors": 0,
  "last_seen_at": "2026-09-26T09:29:51Z",
  "last_error": null,
  "inbound_enabled": false,
  "created_at": "2026-09-03T06:40:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "proxy_name": null,
  "active_campaigns": 1
}
PATCH/accounts/{id}accounts:manage

Change pacing, the proxy or inbound. daily_limit and min_interval_seconds must stay within the platform's bounds; proxy_id null detaches the proxy, a proxy already used by another account is 409 proxy_in_use, and a disabled proxy is a 422 on proxy_id. inbound_enabled true keeps a connection open while the account is ACTIVE and records its private conversations for the AI Seller; turning it off stops that, and turning it on again starts from then rather than replaying the pause.

Example
Request body
{
  "daily_limit": 120,
  "min_interval_seconds": 10,
  "proxy_id": "prx_01j9ra2c4e6g8j0m2p4r6t8w0y",
  "inbound_enabled": true
}
Response · 200 OK
{
  "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "telegram_user_id": 5712093344,
  "display_name": "Silk Road Support",
  "username": "silkroad_support",
  "phone_masked": "+998 •• ••• 45 17",
  "status": "ACTIVE",
  "status_reason": null,
  "status_changed_at": "2026-09-03T06:41:12Z",
  "proxy_id": "prx_01j9ra2c4e6g8j0m2p4r6t8w0y",
  "daily_limit": 120,
  "min_interval_seconds": 10,
  "cooldown_until": null,
  "next_send_at": "2026-09-26T09:30:08Z",
  "sent_today": 42,
  "sent_today_date": "2026-09-26T00:00:00Z",
  "total_sent": 3180,
  "total_failed": 12,
  "consecutive_errors": 0,
  "last_seen_at": "2026-09-26T09:29:51Z",
  "last_error": null,
  "inbound_enabled": true,
  "created_at": "2026-09-03T06:40:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "proxy_name": "Tashkent office",
  "active_campaigns": 1
}
DELETE/accounts/{id}accounts:manage

Log the session out, delete the encrypted session and remove the account. Refused with 409 account_in_use while a running campaign uses it. Delivery history stays.

Example

Response · 204 No Content, no body

POST/accounts/{id}/pauseaccounts:manage

Stop sending from an active account. It keeps its session; its campaigns continue on their other accounts.

Example
Response · 200 OK
{
  "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "telegram_user_id": 5712093344,
  "display_name": "Silk Road Support",
  "username": "silkroad_support",
  "phone_masked": "+998 •• ••• 45 17",
  "status": "PAUSED",
  "status_reason": "Paused by dilnoza@silkroad.example",
  "status_changed_at": "2026-09-03T06:41:12Z",
  "proxy_id": null,
  "daily_limit": 150,
  "min_interval_seconds": 8,
  "cooldown_until": null,
  "next_send_at": "2026-09-26T09:30:08Z",
  "sent_today": 42,
  "sent_today_date": "2026-09-26T00:00:00Z",
  "total_sent": 3180,
  "total_failed": 12,
  "consecutive_errors": 0,
  "last_seen_at": "2026-09-26T09:29:51Z",
  "last_error": null,
  "inbound_enabled": false,
  "created_at": "2026-09-03T06:40:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "proxy_name": null,
  "active_campaigns": 1
}
POST/accounts/{id}/resumeaccounts:manage

Resume a paused, disconnected or errored account. A RESTRICTED account needs acknowledge_restriction: true from a signed-in person, never an API key (403 person_required). Accounts that need to sign in again use reauth instead.

Example
Request body
{
  "acknowledge_restriction": true
}
Response · 200 OK
{
  "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "telegram_user_id": 5712093344,
  "display_name": "Silk Road Support",
  "username": "silkroad_support",
  "phone_masked": "+998 •• ••• 45 17",
  "status": "ACTIVE",
  "status_reason": null,
  "status_changed_at": "2026-09-03T06:41:12Z",
  "proxy_id": null,
  "daily_limit": 150,
  "min_interval_seconds": 8,
  "cooldown_until": null,
  "next_send_at": "2026-09-26T09:30:08Z",
  "sent_today": 42,
  "sent_today_date": "2026-09-26T00:00:00Z",
  "total_sent": 3180,
  "total_failed": 12,
  "consecutive_errors": 0,
  "last_seen_at": "2026-09-26T09:29:51Z",
  "last_error": null,
  "inbound_enabled": false,
  "created_at": "2026-09-03T06:40:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "proxy_name": null,
  "active_campaigns": 1
}
POST/accounts/{id}/checkaccounts:manage

Ask a worker to check the session with Telegram now and update the status.

Example
Response · 200 OK
{
  "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "telegram_user_id": 5712093344,
  "display_name": "Silk Road Support",
  "username": "silkroad_support",
  "phone_masked": "+998 •• ••• 45 17",
  "status": "ACTIVE",
  "status_reason": null,
  "status_changed_at": "2026-09-03T06:41:12Z",
  "proxy_id": null,
  "daily_limit": 150,
  "min_interval_seconds": 8,
  "cooldown_until": null,
  "next_send_at": "2026-09-26T09:30:08Z",
  "sent_today": 42,
  "sent_today_date": "2026-09-26T00:00:00Z",
  "total_sent": 3180,
  "total_failed": 12,
  "consecutive_errors": 0,
  "last_seen_at": "2026-09-26T09:29:51Z",
  "last_error": null,
  "inbound_enabled": false,
  "created_at": "2026-09-03T06:40:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "proxy_name": null,
  "active_campaigns": 1
}
POST/accounts/{id}/reauthaccounts:manage

Sign an AUTH_REQUIRED or DISCONNECTED account in again with the same phone number. Continues like POST /accounts, with the code and password steps.

Example
Request body
{
  "phone": "+998901234517"
}
Response · 200 OK
{
  "state": {
    "flow_id": "flw_01j9r9k3m5p7r9t1v3x5z7b9d1",
    "step": "CODE_SENT",
    "phone_masked": "+998 •• ••• 45 17",
    "expires_at": "2026-09-26T09:40:00Z",
    "code_type": "app"
  },
  "account": null
}
GET/accounts/{id}/activityaccounts:read

The account's latest events and send jobs.

Example
Response · 200 OK
{
  "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "events": [
    {
      "id": "evt_01j9r7m2p4s6v8x0z2b4d6f8h0",
      "type": "account.connected",
      "data": { "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9" },
      "created_at": "2026-09-30T05:00:02Z"
    }
  ],
  "jobs": [
    {
      "id": "job_01j9rc8e0g2j4m6p8r0t2w4y6a",
      "campaign_id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
      "campaign_recipient_id": "crp_01j9rd1f3h5k7m9p1s3v5x7z9b",
      "recipient_id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
      "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "status": "SUCCEEDED",
      "result": "DELIVERED",
      "attempts": 1,
      "last_error": null,
      "created_at": "2026-09-30T08:14:01Z",
      "started_at": "2026-09-30T08:14:01Z",
      "completed_at": "2026-09-30T08:14:02Z"
    }
  ]
}
POST/accounts/{id}/test-messagecampaigns:execute

Send one message from an active account to a recipient (recipient_id) or a public @username (username), to try the account or a draft. Variables are filled in, the account's pacing and daily limit apply, and a failure affects the account just as a campaign send would. Opted-out and suppressed people are refused (422 recipient_opted_out, recipient_suppressed). An organization can send 20 test messages an hour and an account 10 (429 test_limit_reached). Needs an accepted acceptable-use policy.

Example
Request body
{
  "recipient_id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
  "text": "Hi {{first_name|there}}! This is a test from Silk Road Support.",
  "parse_mode": "MARKDOWN"
}
Response · 200 OK
{
  "result": "DELIVERED",
  "message_id": 48301,
  "message": "Delivered. Check the recipient's Telegram.",
  "account_status": "ACTIVE"
}
POST/accounts/{id}/history/importaccounts:manage

Import the account's past private chats so the case analysis can explain conversations from before Oqim. A worker reads them through the account's own session and only reads: nothing is sent and nothing is marked read. It takes private chats with people whose last message is on or after since (YYYY-MM-DD in your organization's time zone, at most 365 days back, 90 by default), at most 500 messages per chat, the newest first; messages from the minute before the import started on are left to the live listener. Groups, channels, bots, Saved Messages, Telegram's service accounts and deleted accounts are skipped, and so are personal chats: the account's contacts (unless include_contacts is true, and even then their conversations stay personal) and chats marked personal. Imported messages keep Telegram's time and are marked imported: they never start an AI turn, a draft or an analysis, open no reply window, don't count as unread and fire no event or webhook, except that a stop request in them still opts the person out. A re-import adds only what's missing. It pauses exactly as long as Telegram asks (FLOOD_WAIT) and resumes where it stopped after a restart. One import per account at a time (409 history_import_running); only ACTIVE accounts (409 account_not_active); since out of range is a 422. channels.history.imported fires once when it's done. Needs an accepted acceptable-use policy.

Example
Request body
{
  "since": "2026-06-29",
  "include_contacts": false
}
Response · 202 Accepted
{
  "status": "RUNNING",
  "since": "2026-06-29",
  "chats_found": 0,
  "chats_imported": 0,
  "chats_skipped_personal": 0,
  "messages_imported": 0,
  "started_at": "2026-09-27T08:00:00Z",
  "finished_at": null,
  "error": null
}
GET/accounts/{id}/historyaccounts:read

The account's latest history import: status IDLE (never imported), RUNNING, DONE, FAILED (error says why, e.g. the account was signed out) or CANCELLED; chats_found are its private chats with people within the period, of which chats_imported were read and chats_skipped_personal left out as personal; messages_imported counts messages added.

Example
Response · 200 OK
{
  "status": "DONE",
  "since": "2026-06-29",
  "chats_found": 48,
  "chats_imported": 41,
  "chats_skipped_personal": 7,
  "messages_imported": 3120,
  "started_at": "2026-09-27T08:00:00Z",
  "finished_at": "2026-09-27T08:14:36Z",
  "error": null
}
POST/accounts/{id}/history/cancelaccounts:manage

Stop the running import. The worker stops before its next page; what was imported stays. 409 history_import_not_running when none is running.

Example
Response · 200 OK
{
  "status": "CANCELLED",
  "since": "2026-06-29",
  "chats_found": 48,
  "chats_imported": 12,
  "chats_skipped_personal": 7,
  "messages_imported": 804,
  "started_at": "2026-09-27T08:00:00Z",
  "finished_at": "2026-09-27T08:03:10Z",
  "error": null
}

Channels: Instagram and Facebook#

Instagram professional accounts (Instagram Login, no Facebook Page needed) and Facebook Pages (Facebook Login for Business) the organization connects, so their direct messages land in the same inbox as Telegram's (GET /ai/conversations?channel=INSTAGRAM|FACEBOOK) and the same AI Seller answers them under Meta's rules: automated replies only within 24 hours of the customer's last message, people up to 7 days with the HUMAN_AGENT tag where the platform has that permission, and never a message to someone who didn't write first. Tokens are encrypted and never returned. An Instagram account or Page is live in one organization at a time. Events: channels.account.updated (connected, status change, disconnected) and channels.history.imported (once, when a history import ends). Until Meta's App Review is done (review_pending), only people with a role on the platform's Meta app can connect. The webhook and the OAuth, deauthorize and data-deletion callbacks are public: Meta and the browser call them, and each verifies what it gets (the signed single-use state, X-Hub-Signature-256, the signed_request).

GET/channels/providersaccounts:read

Which platforms this Oqim platform can connect: configured (the platform owner set up the Meta app; otherwise connecting is 409 provider_not_configured), review_pending (only accounts with a role on the Meta app can connect yet), human_agent (people may answer up to 7 days after a customer's last message) and history_messages_per_thread (history imports read only this many of a thread's most recent messages: Meta's limit).

Example
Response · 200 OK
{
  "instagram": {
    "configured": true,
    "review_pending": true,
    "human_agent": false,
    "history_messages_per_thread": 20
  },
  "facebook": {
    "configured": true,
    "review_pending": true,
    "human_agent": false,
    "history_messages_per_thread": 20
  },
  "youtube": { "configured": false }
}
POST/channels/instagram/connectaccounts:manage

The Instagram Login URL to send the person to. Its state is signed, lasts 10 minutes and works once. After they allow access, Instagram returns them to the callback, which connects the account and sends them to the console's /channels page. A person signed in to the console connects accounts (API keys get 403 person_required); 409 provider_not_configured when the platform has no Instagram app. Needs an accepted acceptable-use policy.

Example
Response · 200 OK
{
  "url": "https://www.instagram.com/oauth/authorize?client_id=990602627938098&redirect_uri=https%3A%2F%2Fapp.example.com%2Fapi%2Fv1%2Fchannels%2Finstagram%2Fcallback&response_type=code&scope=instagram_business_basic%2Cinstagram_business_manage_messages%2Cinstagram_business_manage_comments%2Cinstagram_business_manage_insights&state=…"
}
GET/channels/instagram/callbackPublic

Where Instagram returns the browser (public; the state is verified and used once, and the browser must carry the console session of the person who started the flow, so nobody can be tricked into connecting their account to another organization). The code becomes a 60-day token, the account is read, stored with capabilities from the permissions granted, subscribed to its webhooks (messages, messaging_postbacks, message_reactions, messaging_seen, and comments and mentions when granted) and its history import starts. Redirects (302) to PUBLIC_APP_URL/channels?connected=<id>, or ?error= denied, state_invalid, already_connected_elsewhere, missing_permissions (messaging must be allowed) or provider_error.

Query: code, state, error, error_reason

Example
Response · 302 Found
HTTP/1.1 302 Found
Location: https://app.example.com/channels?connected=cha_01j9s7b2d4f6h8k0m2p4s6v8x0
POST/channels/facebook/connectaccounts:manage

The Facebook Login for Business URL (with the platform's login configuration when it has one, else the pages_* permissions). Same rules as Instagram's.

Example
Response · 200 OK
{
  "url": "https://www.facebook.com/v25.0/dialog/oauth?client_id=442224939723604&redirect_uri=https%3A%2F%2Fapp.example.com%2Fapi%2Fv1%2Fchannels%2Ffacebook%2Fcallback&response_type=code&state=…&scope=pages_show_list%2Cpages_manage_metadata%2Cpages_messaging%2Cpages_read_engagement%2Cpages_read_user_content%2Cbusiness_management"
}
GET/channels/facebook/callbackPublic

Where Facebook returns the browser (public; state-verified and bound to the starter's console session, as for Instagram). Every Page the person granted is connected with its Page token and subscribed to messages, messaging_postbacks, message_echoes, message_reactions, message_reads (and feed), and its history import starts. Pages live in another organization are skipped. Redirects to PUBLIC_APP_URL/channels?connected=<id,id>, or ?error= as for Instagram.

Query: code, state, error, error_reason

Example
Response · 302 Found
HTTP/1.1 302 Found
Location: https://app.example.com/channels?connected=cha_01j9t0b3d5f7h9k1m3p5r7t9v1
GET/channels/accountsaccounts:read

Connected accounts (Instagram, Facebook and YouTube), oldest first. Instagram accounts and Pages have stats: their conversations and those whose 24-hour window is open. Filter by platform (INSTAGRAM, FACEBOOK, YOUTUBE) and status (CONNECTED, PUBLIC, AUTH_REQUIRED, ERROR, DISCONNECTED).

Query: platform, status, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "cha_01j9s7b2d4f6h8k0m2p4s6v8x0",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "platform": "INSTAGRAM",
      "external_id": "17841400000000001",
      "name": "Silk Road Events",
      "username": "silkroad.events",
      "avatar_url": "https://scontent.cdninstagram.com/v/avatar.jpg",
      "status": "CONNECTED",
      "status_reason": null,
      "status_changed_at": "2026-09-20T08:00:00Z",
      "capabilities": ["MESSAGING", "COMMENTS", "CONTENT"],
      "scopes": [
        "instagram_business_basic",
        "instagram_business_manage_messages",
        "instagram_business_manage_comments"
      ],
      "token_expires_at": "2026-11-19T08:00:00Z",
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "ai_enabled": true,
      "settings": {},
      "connected_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "last_sync_at": null,
      "created_at": "2026-09-20T08:00:00Z",
      "updated_at": "2026-09-26T09:30:00Z",
      "stats": { "conversations": 42, "open_windows": 7 }
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/channels/accounts/{id}accounts:read

One connected account, with its stats.

Example
Response · 200 OK
{
  "id": "cha_01j9s7b2d4f6h8k0m2p4s6v8x0",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "platform": "INSTAGRAM",
  "external_id": "17841400000000001",
  "name": "Silk Road Events",
  "username": "silkroad.events",
  "avatar_url": "https://scontent.cdninstagram.com/v/avatar.jpg",
  "status": "CONNECTED",
  "status_reason": null,
  "status_changed_at": "2026-09-20T08:00:00Z",
  "capabilities": ["MESSAGING", "COMMENTS", "CONTENT"],
  "scopes": [
    "instagram_business_basic",
    "instagram_business_manage_messages",
    "instagram_business_manage_comments"
  ],
  "token_expires_at": "2026-11-19T08:00:00Z",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "ai_enabled": true,
  "settings": {},
  "connected_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "last_sync_at": null,
  "created_at": "2026-09-20T08:00:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "stats": { "conversations": 42, "open_windows": 7 }
}
PATCH/channels/accounts/{id}accounts:manage

The business the account serves (business_id; null clears it), whether its AI Seller answers the account's direct messages (ai_enabled: needs a business and the MESSAGING capability, otherwise a 422 on ai_enabled) and settings (an object). Audited.

Example
Request body
{
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "ai_enabled": true
}
Response · 200 OK
{
  "id": "cha_01j9s7b2d4f6h8k0m2p4s6v8x0",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "platform": "INSTAGRAM",
  "external_id": "17841400000000001",
  "name": "Silk Road Events",
  "username": "silkroad.events",
  "avatar_url": "https://scontent.cdninstagram.com/v/avatar.jpg",
  "status": "CONNECTED",
  "status_reason": null,
  "status_changed_at": "2026-09-20T08:00:00Z",
  "capabilities": ["MESSAGING", "COMMENTS", "CONTENT"],
  "scopes": [
    "instagram_business_basic",
    "instagram_business_manage_messages",
    "instagram_business_manage_comments"
  ],
  "token_expires_at": "2026-11-19T08:00:00Z",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "ai_enabled": true,
  "settings": {},
  "connected_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "last_sync_at": null,
  "created_at": "2026-09-20T08:00:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "stats": { "conversations": 42, "open_windows": 7 }
}
DELETE/channels/accounts/{id}accounts:manage

Disconnect the account: its webhook subscription is removed (best effort), its tokens deleted and its status DISCONNECTED. Its conversations and their history stay. It may then be connected by another organization.

Example

Response · 204 No Content, no body

POST/channels/accounts/{id}/checkaccounts:manage

Check the account with Meta now: its profile is read again and its webhook subscription renewed. The status follows: CONNECTED, AUTH_REQUIRED (the token expired or was revoked; reconnect) or ERROR with the reason. 502 provider_unavailable when Meta can't be reached.

Example
Response · 200 OK
{
  "id": "cha_01j9s7b2d4f6h8k0m2p4s6v8x0",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "platform": "INSTAGRAM",
  "external_id": "17841400000000001",
  "name": "Silk Road Events",
  "username": "silkroad.events",
  "avatar_url": "https://scontent.cdninstagram.com/v/avatar.jpg",
  "status": "CONNECTED",
  "status_reason": null,
  "status_changed_at": "2026-09-20T08:00:00Z",
  "capabilities": ["MESSAGING", "COMMENTS", "CONTENT"],
  "scopes": [
    "instagram_business_basic",
    "instagram_business_manage_messages",
    "instagram_business_manage_comments"
  ],
  "token_expires_at": "2026-11-19T08:00:00Z",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "ai_enabled": true,
  "settings": {},
  "connected_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "last_sync_at": null,
  "created_at": "2026-09-20T08:00:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "stats": { "conversations": 42, "open_windows": 7 }
}
POST/channels/accounts/{id}/history/importaccounts:manage

Import the account's past direct messages from Meta's Conversations API: threads active since since (a date, at most 365 days back; 90 days by default), and of each only the 20 most recent messages (Meta's limit), paced at 2 calls a second. Imported messages keep Meta's times, are marked imported, and never start an AI turn, open a reply window, raise the unread count or fire per-message events; a stop request found in them still stops the conversation. Importing again adds only what's missing. An import already running is returned as it is. 409 history_unavailable when the account can't read its messages. It also starts when an account connects.

Example
Request body
{
  "since": "2026-06-29"
}
Response · 202 Accepted
{
  "status": "RUNNING",
  "since": "2026-06-29",
  "chats_found": 0,
  "chats_imported": 0,
  "chats_skipped_personal": 0,
  "messages_imported": 0,
  "started_at": "2026-09-20T08:00:05Z",
  "finished_at": null,
  "error": null
}
GET/channels/accounts/{id}/historyaccounts:read

The account's latest history import: status IDLE (never ran), RUNNING, DONE, FAILED or CANCELLED, with its counts. chats_skipped_personal is always 0 on Instagram and Facebook.

Example
Response · 200 OK
{
  "status": "DONE",
  "since": "2026-06-29",
  "chats_found": 38,
  "chats_imported": 35,
  "chats_skipped_personal": 0,
  "messages_imported": 512,
  "started_at": "2026-09-20T08:00:05Z",
  "finished_at": "2026-09-20T08:01:40Z",
  "error": null
}
POST/channels/accounts/{id}/history/cancelaccounts:manage

Stop a running import; what it stored stays. Returns the import.

Example
Response · 200 OK
{
  "status": "CANCELLED",
  "since": "2026-06-29",
  "chats_found": 38,
  "chats_imported": 35,
  "chats_skipped_personal": 0,
  "messages_imported": 512,
  "started_at": "2026-09-20T08:00:05Z",
  "finished_at": "2026-09-20T08:01:40Z",
  "error": null
}
GET/channels/automationsaccounts:read

List comment-to-DM automations. ?connected_account_id= filters to one account. Newest first.

Query: connected_account_id, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "cau_01j9r6d7f9k1m3p5r7t9v1w3x",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "account_id": "cha_…",
      "platform": "INSTAGRAM",
      "name": "Price questions",
      "enabled": true,
      "posts_scope": "ALL",
      "post_ids": [],
      "trigger": "JEV_PURCHASE_INTEREST",
      "keywords": [],
      "threshold": 0.7,
      "reply_mode": "TEMPLATE",
      "templates": { "uz": "Rahmat! Narxni DMda yubordik …", "ru": "…", "en": "…" },
      "hourly_cap": 100,
      "sent_last_hour": 3,
      "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "created_at": "2026-09-28T00:00:00Z",
      "updated_at": "2026-09-28T00:00:00Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/channels/automationsaccounts:manage

Create a comment-to-DM automation on one Instagram account or Facebook Page. When the trigger fires on a comment on the account's own post (within 7 days), the private reply goes out once, within the hourly cap.

422 validation_error names the field (scope, keywords, templates, threshold, hourly_cap); 404 account_not_found.

Example
Request body
{
  "connected_account_id": "cha_…",
  "name": "Price questions (optional)",
  "enabled": true,
  "posts_scope": "ALL | SELECTED (with SELECTED, only the posts in post_ids)",
  "post_ids": [
    "external post ids (social_posts.external_id) when posts_scope=SELECTED"
  ],
  "trigger": "KEYWORDS | JEV_PURCHASE_INTEREST",
  "keywords": "price,qancha? (required for KEYWORDS)",
  "threshold": "0.7 (JEV's confidence for JEV_PURCHASE_INTEREST, 0-1)",
  "reply_mode": "TEMPLATE | SELLER",
  "templates": "{ uz, ru, en } (at least one non-empty variant for TEMPLATE)",
  "hourly_cap": "100 (at most 750, Meta's limit)"
}
Response · 200 OK
{
  "id": "cau_01j9r6d7f9k1m3p5r7t9v1w3x",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "account_id": "cha_…",
  "platform": "INSTAGRAM",
  "name": "Price questions",
  "enabled": true,
  "posts_scope": "ALL",
  "post_ids": [],
  "trigger": "JEV_PURCHASE_INTEREST",
  "keywords": [],
  "threshold": 0.7,
  "reply_mode": "TEMPLATE",
  "templates": { "uz": "Rahmat! Narxni DMda yubordik …", "ru": "…", "en": "…" },
  "hourly_cap": 100,
  "sent_last_hour": 3,
  "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "created_at": "2026-09-28T00:00:00Z",
  "updated_at": "2026-09-28T00:00:00Z"
}
GET/channels/automations/{id}accounts:read

One automation, with sent_last_hour: what went out in the last hour, against the cap.

Example
Response · 200 OK
{
  "id": "cau_01j9r6d7f9k1m3p5r7t9v1w3x",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "account_id": "cha_…",
  "platform": "INSTAGRAM",
  "name": "Price questions",
  "enabled": true,
  "posts_scope": "ALL",
  "post_ids": [],
  "trigger": "JEV_PURCHASE_INTEREST",
  "keywords": [],
  "threshold": 0.7,
  "reply_mode": "TEMPLATE",
  "templates": { "uz": "Rahmat! Narxni DMda yubordik …", "ru": "…", "en": "…" },
  "hourly_cap": 100,
  "sent_last_hour": 3,
  "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "created_at": "2026-09-28T00:00:00Z",
  "updated_at": "2026-09-28T00:00:00Z"
}
PATCH/channels/automations/{id}accounts:manage

Change the fields the body carries; the same rules as on create apply.

Example
Request body
{ ...any create field }
Response · 200 OK
{
  "id": "cau_01j9r6d7f9k1m3p5r7t9v1w3x",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "account_id": "cha_…",
  "platform": "INSTAGRAM",
  "name": "Price questions",
  "enabled": true,
  "posts_scope": "ALL",
  "post_ids": [],
  "trigger": "JEV_PURCHASE_INTEREST",
  "keywords": [],
  "threshold": 0.7,
  "reply_mode": "TEMPLATE",
  "templates": { "uz": "Rahmat! Narxni DMda yubordik …", "ru": "…", "en": "…" },
  "hourly_cap": 100,
  "sent_last_hour": 3,
  "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "created_at": "2026-09-28T00:00:00Z",
  "updated_at": "2026-09-28T00:00:00Z"
}
DELETE/channels/automations/{id}accounts:manage

Delete the automation. Its past private replies stay in the log (their automation_id becomes null).

Example
Response · 200 OK
HTTP/1.1 204 No Content
GET/channels/accounts/{id}/private-repliesaccounts:read

The private-reply log of an account: every comment an automation considered and what it did. ?automation_id= and ?status= (PENDING, SENDING, SENT, FAILED, SKIPPED) filter.

Query: automation_id, status, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "cpr_01j9r6e8g0k2m4p6r8t0v2w4x",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "account_id": "cha_…",
      "automation_id": "cau_…",
      "platform": "INSTAGRAM",
      "comment_id": "17891…",
      "post_external_id": "17902…",
      "post_id": "spo_…",
      "social_comment_id": null,
      "parent_comment_id": null,
      "commenter_id": "54321…",
      "commenter": "@dilnoza.uz",
      "comment_text": "Narxi qancha?",
      "comment_created_at": "2026-09-28T00:00:00Z",
      "source": "WEBHOOK",
      "status": "SENT",
      "skip_reason": null,
      "trigger": "JEV_PURCHASE_INTEREST",
      "matched_keyword": null,
      "jev_probability": 0.91,
      "reply_mode": "TEMPLATE",
      "language": "uz",
      "reply_text": "Rahmat! Narx …",
      "execution_id": null,
      "recipient_id": "54321…",
      "message_id": "…",
      "error_code": null,
      "error": null,
      "attempts": 1,
      "due_at": null,
      "sent_at": "2026-09-28T00:01:00Z",
      "created_at": "2026-09-28T00:00:30Z",
      "updated_at": "2026-09-28T00:01:00Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/channels/accounts/{id}/marketing-topicsaccounts:read

A Facebook Page's marketing-message topics: who opted in, who is eligible right now (outside Meta's frequency limits), who stopped or whose token expired. Instagram has no marketing messages.

Example
Response · 200 OK
{
  "data": [
    {
      "topic": "Weekly deals",
      "active": 12,
      "eligible": 9,
      "stopped": 2,
      "expired": 1,
      "last_opt_in_at": "…"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/channels/accounts/{id}/marketing-subscriptionsaccounts:read

The people who opted in to a Page's marketing messages. ?topic= and ?status= (ACTIVE, STOPPED, EXPIRED) filter. Tokens are never returned.

Query: topic, status, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "mms_01j9r6f9h1k3m5p7r9t1v3w5x",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "account_id": "cha_…",
      "conversation_id": "cnv_…",
      "recipient_id": "psid-12345",
      "topic": "Weekly deals",
      "status": "ACTIVE",
      "token_status": "NOTIFICATION_MESSAGES_TOKEN",
      "timezone": "Asia/Tashkent",
      "token_expires_at": null,
      "next_eligible_at": "2026-09-29T00:00:00Z",
      "last_sent_at": null,
      "opted_in_at": "2026-09-28T00:00:00Z",
      "stopped_at": null,
      "created_at": "2026-09-28T00:00:00Z",
      "updated_at": "2026-09-28T00:00:00Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/channels/meta/webhookPublic

Meta's subscription handshake: with hub.mode subscribe and the platform's hub.verify_token, the answer is hub.challenge as plain text; otherwise 403.

Query: hub.mode, hub.verify_token, hub.challenge

Example
Response · 200 OK
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8

1158201444
POST/channels/meta/webhookPublic

Meta's notifications for Instagram (object instagram) and Pages (object page), signed with X-Hub-Signature-256 (401 signature_missing, signature_invalid). Each item (a message, an echo, a reaction, a read, an unsend, an edit) is stored once, deduplicated by its kind and mid, and processed by a worker; the answer comes at once. Items for accounts no organization has live are ignored.

Example
Request body
{
  "object": "instagram",
  "entry": [
    {
      "id": "17841400000000001",
      "time": 1790000000000,
      "messaging": [
        {
          "sender": { "id": "1784120000000042" },
          "recipient": { "id": "17841400000000001" },
          "timestamp": 1790000000000,
          "message": { "mid": "aWdfZAG1…", "text": "Salom! Narxi qancha?" }
        }
      ]
    }
  ]
}
Response · 200 OK
{
  "accepted": 1,
  "duplicates": 0,
  "ignored": 0
}
POST/channels/meta/deauthorizePublic

Meta's deauthorize callback (form field signed_request, signed with the Meta or Instagram app's secret; 400 invalid_signed_request otherwise): every account the person connected loses its tokens and becomes DISCONNECTED, in every organization.

Example
Request body
curl -X POST https://app.example.com/api/v1/channels/meta/deauthorize \
  -F signed_request=<signature>.<payload>
Response · 200 OK
{
  "success": true,
  "accounts": 1
}
POST/channels/meta/data-deletionPublic

Meta's data-deletion callback (signed_request as above): the person's accounts lose their tokens and profile and become DISCONNECTED, and their conversations and messages are deleted. The answer is the status URL and the confirmation code Meta shows the person.

Example
Request body
curl -X POST https://app.example.com/api/v1/channels/meta/data-deletion \
  -F signed_request=<signature>.<payload>
Response · 200 OK
{
  "url": "https://app.example.com/api/v1/channels/meta/data-deletion/8f14e45fceea167a5a36dedd",
  "confirmation_code": "8f14e45fceea167a5a36dedd"
}
GET/channels/meta/data-deletion/{code}Public

The status of a data-deletion request (RECEIVED or COMPLETED). 404 for an unknown code.

Example
Response · 200 OK
{
  "confirmation_code": "8f14e45fceea167a5a36dedd",
  "status": "COMPLETED",
  "requested_at": "2026-09-26T09:30:00Z",
  "completed_at": "2026-09-26T09:30:00Z"
}

Proxies#

Network routes your organization controls, pinned to at most one account each. Oqim checks reachability; it never rotates proxies.

GET/proxiesproxies:read

Your proxies with their last health check.

Query: status, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "prx_01j9ra2c4e6g8j0m2p4r6t8w0y",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "name": "Tashkent office",
      "protocol": "SOCKS5",
      "host": "proxy.silkroad.example",
      "port": 1080,
      "username": "oqim",
      "status": "ACTIVE",
      "latency_ms": 84,
      "last_checked_at": "2026-09-26T09:30:00Z",
      "last_error": null,
      "disabled_reason": null,
      "disabled_at": null,
      "created_at": "2026-09-03T06:20:00Z",
      "updated_at": "2026-09-26T09:30:00Z",
      "has_credentials": true,
      "assigned_account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "assigned_account_name": "Silk Road Support"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/proxiesproxies:manage

Add a SOCKS5 or HTTP (CONNECT) proxy with an optional username and password, or an MTProto proxy with its secret. Credentials are encrypted at rest and never returned. An address (protocol, host and port) a platform administrator disabled can't be added again: 409 proxy_disabled.

Example
Request body
{
  "name": "Tashkent office",
  "protocol": "SOCKS5",
  "host": "proxy.silkroad.example",
  "port": 1080,
  "username": "oqim",
  "password": "…"
}
Response · 201 Created
{
  "id": "prx_01j9ra2c4e6g8j0m2p4r6t8w0y",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Tashkent office",
  "protocol": "SOCKS5",
  "host": "proxy.silkroad.example",
  "port": 1080,
  "username": "oqim",
  "status": "ACTIVE",
  "latency_ms": 84,
  "last_checked_at": "2026-09-26T09:30:00Z",
  "last_error": null,
  "disabled_reason": null,
  "disabled_at": null,
  "created_at": "2026-09-03T06:20:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "has_credentials": true,
  "assigned_account_id": null,
  "assigned_account_name": null
}
POST/proxies/importproxies:manage

Add up to 1,000 proxies at once from text in a JSON body, or from a file in a multipart field named file (2 MB at most). Oqim reads one proxy per line (socks5://user:pass@host:port, host:port, host:port:user:pass or a tg://proxy link), a CSV with a header row, or a JSON list. Lines without a scheme use default_protocol, SOCKS5 or HTTP. Send dry_run=true first for the report; the real import answers 201 and checks each new proxy right away. Proxies you already have are skipped, and an address a platform administrator disabled is reported INVALID.

Example
Request body
{
  "text": "socks5://oqim:…@proxy.silkroad.example:1080\nhttp://203.0.113.40:8080\n203.0.113.41:1080:office:…",
  "default_protocol": "SOCKS5",
  "name_prefix": "Office",
  "dry_run": true
}
Response · 200 OK
{
  "dry_run": true,
  "total": 3,
  "ready": 2,
  "duplicates": 1,
  "invalid": 0,
  "created": 0,
  "items": [
    {
      "line": 1,
      "raw": "socks5://oqim:…@proxy.silkroad.example:1080",
      "protocol": "SOCKS5",
      "host": "proxy.silkroad.example",
      "port": 1080,
      "has_auth": true,
      "status": "DUPLICATE",
      "message": "Already in your proxies."
    },
    {
      "line": 2,
      "raw": "http://203.0.113.40:8080",
      "protocol": "HTTP",
      "host": "203.0.113.40",
      "port": 8080,
      "has_auth": false,
      "status": "READY"
    },
    {
      "line": 3,
      "raw": "203.0.113.41:1080:office:…",
      "protocol": "SOCKS5",
      "host": "203.0.113.41",
      "port": 1080,
      "has_auth": true,
      "status": "READY"
    }
  ]
}
POST/proxies/bulkproxies:manage

Act on many proxies at once, chosen by proxy_ids (up to 5,000), by status such as UNREACHABLE, or both. action "check" queues a health check for each and answers 202 with queued; "delete" removes them, detaching any account that used one, and answers 200 with deleted; proxies a platform administrator disabled stay, counted in kept_disabled.

Example
Request body
{
  "action": "check",
  "status": "UNREACHABLE"
}
Response · 202 Accepted
{
  "queued": 4
}
GET/proxies/{id}proxies:read

One proxy.

Example
Response · 200 OK
{
  "id": "prx_01j9ra2c4e6g8j0m2p4r6t8w0y",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Tashkent office",
  "protocol": "SOCKS5",
  "host": "proxy.silkroad.example",
  "port": 1080,
  "username": "oqim",
  "status": "ACTIVE",
  "latency_ms": 84,
  "last_checked_at": "2026-09-26T09:30:00Z",
  "last_error": null,
  "disabled_reason": null,
  "disabled_at": null,
  "created_at": "2026-09-03T06:20:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "has_credentials": true,
  "assigned_account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "assigned_account_name": "Silk Road Support"
}
PATCH/proxies/{id}proxies:manage

Change a proxy. Omit password to keep the stored one. A proxy a platform administrator disabled (status DISABLED, with disabled_reason) can't be changed, and no proxy can be moved to its address: 409 proxy_disabled.

Example
Request body
{
  "name": "Tashkent office (backup line)",
  "port": 1081
}
Response · 200 OK
{
  "id": "prx_01j9ra2c4e6g8j0m2p4r6t8w0y",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Tashkent office (backup line)",
  "protocol": "SOCKS5",
  "host": "proxy.silkroad.example",
  "port": 1081,
  "username": "oqim",
  "status": "ACTIVE",
  "latency_ms": 84,
  "last_checked_at": "2026-09-26T09:30:00Z",
  "last_error": null,
  "disabled_reason": null,
  "disabled_at": null,
  "created_at": "2026-09-03T06:20:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "has_credentials": true,
  "assigned_account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "assigned_account_name": "Silk Road Support"
}
DELETE/proxies/{id}proxies:manage

Delete a proxy. An account using it is detached and connects directly. A proxy a platform administrator disabled stays until they enable it: 409 proxy_disabled.

Example

Response · 204 No Content, no body

POST/proxies/{id}/checkproxies:manage

Dial a Telegram data centre through the proxy now; updates status and latency_ms. A disabled proxy isn't checked: 409 proxy_disabled.

Example
Response · 200 OK
{
  "id": "prx_01j9ra2c4e6g8j0m2p4r6t8w0y",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Tashkent office",
  "protocol": "SOCKS5",
  "host": "proxy.silkroad.example",
  "port": 1080,
  "username": "oqim",
  "status": "ACTIVE",
  "latency_ms": 84,
  "last_checked_at": "2026-09-26T09:30:00Z",
  "last_error": null,
  "disabled_reason": null,
  "disabled_at": null,
  "created_at": "2026-09-03T06:20:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "has_credentials": true,
  "assigned_account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "assigned_account_name": "Silk Road Support"
}
GET/proxies/{id}/checksproxies:read

Recent health checks, newest first.

Example
Response · 200 OK
{
  "data": [
    {
      "id": 5521,
      "proxy_id": "prx_01j9ra2c4e6g8j0m2p4r6t8w0y",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "status": "ACTIVE",
      "latency_ms": 84,
      "error": null,
      "checked_at": "2026-09-26T09:30:00Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

Recipients#

People you may message and your basis for doing so. See Recipients for consent states and the CSV format.

GET/recipientsrecipients:read

Search and filter recipients.

Query: q, consent, tag, list_id, source, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "telegram_id": 123456789,
      "username": "aziza_k",
      "first_name": "Aziza",
      "last_name": "Karimova",
      "language": "uz",
      "source": "CSV",
      "consent_status": "OPTED_IN",
      "consent_timestamp": "2026-08-14T11:20:00Z",
      "consent_note": "Registered for Tashkent Expo 2026 on expo.example",
      "opted_out_at": null,
      "opt_out_source": null,
      "attributes": { "event_date": "1 October", "location": "Tashkent City Hall" },
      "tags": ["expo-2026", "vip"],
      "imported_at": "2026-09-05T09:00:00Z",
      "created_at": "2026-09-05T09:00:00Z",
      "updated_at": "2026-09-05T09:00:00Z",
      "opt_out_status": false,
      "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"]
    }
  ],
  "total": 4210,
  "limit": 50,
  "offset": 0
}
POST/recipientsrecipients:manage

Add one recipient. telegram_id or username is required; everything else is optional. A Telegram ID or username that already exists is 409 recipient_exists.

Example
Request body
{
  "telegram_id": 123456789,
  "username": "aziza_k",
  "first_name": "Aziza",
  "last_name": "Karimova",
  "consent_status": "OPTED_IN",
  "consent_note": "Registered for Tashkent Expo 2026 on expo.example",
  "tags": ["expo-2026", "vip"],
  "attributes": { "event_date": "1 October", "location": "Tashkent City Hall" },
  "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"]
}
Response · 201 Created
{
  "id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "telegram_id": 123456789,
  "username": "aziza_k",
  "first_name": "Aziza",
  "last_name": "Karimova",
  "language": "uz",
  "source": "API",
  "consent_status": "OPTED_IN",
  "consent_timestamp": "2026-08-14T11:20:00Z",
  "consent_note": "Registered for Tashkent Expo 2026 on expo.example",
  "opted_out_at": null,
  "opt_out_source": null,
  "attributes": { "event_date": "1 October", "location": "Tashkent City Hall" },
  "tags": ["expo-2026", "vip"],
  "imported_at": "2026-09-05T09:00:00Z",
  "created_at": "2026-09-05T09:00:00Z",
  "updated_at": "2026-09-05T09:00:00Z",
  "opt_out_status": false,
  "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"]
}
POST/recipients/importrecipients:manage

Import a CSV or JSON file (multipart), or a JSON body with records. Send dry_run=true first for the report without saving anything, then the same data with dry_run=false. Up to 10 MB and 100,000 rows.

Example
Request (multipart/form-data)
curl https://app.example.com/api/v1/recipients/import \
  -H "Authorization: Bearer $OQIM_API_KEY" \
  -F file=@expo-attendees.csv \
  -F dry_run=true \
  -F default_consent=UNKNOWN \
  -F list_id=lst_01j9r5e2g4j6m8p0r2t4w6y8a0
Response · 200 OK
{
  "dry_run": true,
  "total": 1950,
  "valid": 1923,
  "created": 0,
  "updated": 0,
  "duplicates": 12,
  "invalid": 9,
  "suppressed": 6,
  "opted_out": 0,
  "by_consent": {
    "OPTED_IN": 1402,
    "AUTHORIZED": 0,
    "EXISTING_RELATIONSHIP": 496,
    "UNKNOWN": 25
  },
  "issues": [
    {
      "row": 14,
      "field": "telegram_id",
      "value": "12ab",
      "kind": "INVALID",
      "message": "Telegram IDs are positive whole numbers."
    },
    {
      "row": 88,
      "field": "telegram_id",
      "value": "987654321",
      "kind": "DUPLICATE",
      "message": "Same Telegram ID as row 31; skipped."
    },
    {
      "row": 402,
      "kind": "SUPPRESSED",
      "message": "On the suppression list, so imported as opted out."
    },
    {
      "row": 977,
      "kind": "UNKNOWN_CONSENT",
      "message": "Consent is unknown, so campaigns skip this recipient unless platform policy allows it."
    }
  ],
  "columns": [
    "telegram_id",
    "username",
    "first_name",
    "last_name",
    "consent_status",
    "event_date",
    "location"
  ],
  "sample": [
    {
      "telegram_id": 123456789,
      "username": "aziza_k",
      "first_name": "Aziza",
      "last_name": "Karimova",
      "consent_status": "OPTED_IN"
    }
  ]
}
POST/recipients/bulkrecipients:manage

Apply one action to up to 5,000 recipients: tag, untag, add_to_list, remove_from_list, set_consent, opt_out or delete. set_consent can't opt anyone out; use opt_out. in_flight counts recipients skipped because a message to them was being sent.

Example
Request body
{
  "action": "add_to_list",
  "recipient_ids": ["rcp_01j9r5b3d5f7h9k1m3p5r7t9v1", "rcp_01j9s3d5f7h9k1m3p5r7t9v1x3"],
  "list_id": "lst_01j9r5e2g4j6m8p0r2t4w6y8a0"
}
Response · 200 OK
{
  "action": "add_to_list",
  "requested": 2,
  "matched": 2,
  "updated": 2,
  "skipped": 0,
  "in_flight": 0
}
GET/recipients/tagsrecipients:read

Every tag in use with its recipient count.

Example
Response · 200 OK
{
  "data": [
    { "tag": "expo-2026", "count": 1902 },
    { "tag": "vip", "count": 64 }
  ],
  "total": 2,
  "limit": 50,
  "offset": 0
}
GET/recipients/{id}recipients:read

One recipient, with the lists it belongs to.

Example
Response · 200 OK
{
  "id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "telegram_id": 123456789,
  "username": "aziza_k",
  "first_name": "Aziza",
  "last_name": "Karimova",
  "language": "uz",
  "source": "CSV",
  "consent_status": "OPTED_IN",
  "consent_timestamp": "2026-08-14T11:20:00Z",
  "consent_note": "Registered for Tashkent Expo 2026 on expo.example",
  "opted_out_at": null,
  "opt_out_source": null,
  "attributes": { "event_date": "1 October", "location": "Tashkent City Hall" },
  "tags": ["expo-2026", "vip"],
  "imported_at": "2026-09-05T09:00:00Z",
  "created_at": "2026-09-05T09:00:00Z",
  "updated_at": "2026-09-05T09:00:00Z",
  "opt_out_status": false,
  "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"]
}
PATCH/recipients/{id}recipients:manage

Update a recipient. An opted-out recipient's consent can't be changed (409 recipient_opted_out).

Example
Request body
{
  "tags": ["expo-2026", "vip", "speaker"],
  "attributes": { "event_date": "1 October", "location": "Hall B" }
}
Response · 200 OK
{
  "id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "telegram_id": 123456789,
  "username": "aziza_k",
  "first_name": "Aziza",
  "last_name": "Karimova",
  "language": "uz",
  "source": "CSV",
  "consent_status": "OPTED_IN",
  "consent_timestamp": "2026-08-14T11:20:00Z",
  "consent_note": "Registered for Tashkent Expo 2026 on expo.example",
  "opted_out_at": null,
  "opt_out_source": null,
  "attributes": { "event_date": "1 October", "location": "Tashkent City Hall" },
  "tags": ["expo-2026", "vip", "speaker"],
  "imported_at": "2026-09-05T09:00:00Z",
  "created_at": "2026-09-05T09:00:00Z",
  "updated_at": "2026-09-05T09:00:00Z",
  "opt_out_status": false,
  "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"]
}
DELETE/recipients/{id}recipients:manage

Delete a recipient. If they opted out, their suppression entry stays, so they're never messaged again. 409 recipient_busy while a message to them is being sent.

Example

Response · 204 No Content, no body

POST/recipients/{id}/opt-outrecipients:manage

Record an opt-out the person made outside Oqim. Permanent and organization-wide: adds them to the suppression list, skips their pending sends and emits recipient.opted_out.

Example
Response · 200 OK
{
  "id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "telegram_id": 123456789,
  "username": "aziza_k",
  "first_name": "Aziza",
  "last_name": "Karimova",
  "language": "uz",
  "source": "CSV",
  "consent_status": "OPTED_OUT",
  "consent_timestamp": "2026-08-14T11:20:00Z",
  "consent_note": "Registered for Tashkent Expo 2026 on expo.example",
  "opted_out_at": "2026-09-26T09:30:00Z",
  "opt_out_source": "MANUAL",
  "attributes": { "event_date": "1 October", "location": "Tashkent City Hall" },
  "tags": ["expo-2026", "vip"],
  "imported_at": "2026-09-05T09:00:00Z",
  "created_at": "2026-09-05T09:00:00Z",
  "updated_at": "2026-09-05T09:00:00Z",
  "opt_out_status": true,
  "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"]
}
GET/recipient-importsrecipients:read

Past imports and their counts.

Example
Response · 200 OK
{
  "data": [
    {
      "id": "imp_01j9rh3k5m7p9s1t3v5x7z9b1d",
      "source": "CSV",
      "filename": "expo-attendees.csv",
      "list_id": "lst_01j9r5e2g4j6m8p0r2t4w6y8a0",
      "total": 1950,
      "created": 1901,
      "updated": 22,
      "duplicates": 12,
      "invalid": 9,
      "suppressed": 6,
      "created_at": "2026-09-05T09:00:00Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

Lists#

Named groups of recipients. Deleting a list keeps its recipients.

GET/listsrecipients:read

Lists with member and sendable counts.

Example
Response · 200 OK
{
  "data": [
    {
      "id": "lst_01j9r5e2g4j6m8p0r2t4w6y8a0",
      "name": "Expo 2026 attendees",
      "description": "Registered through the expo website",
      "created_at": "2026-09-05T08:55:00Z",
      "updated_at": "2026-09-05T09:00:00Z",
      "member_count": 1902,
      "sendable_count": 1840
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/listsrecipients:manage

Create a list.

Example
Request body
{
  "name": "Expo 2026 attendees",
  "description": "Registered through the expo website"
}
Response · 201 Created
{
  "id": "lst_01j9r5e2g4j6m8p0r2t4w6y8a0",
  "name": "Expo 2026 attendees",
  "description": "Registered through the expo website",
  "created_at": "2026-09-05T08:55:00Z",
  "updated_at": "2026-09-05T09:00:00Z",
  "member_count": 0,
  "sendable_count": 0
}
GET/lists/{id}recipients:read

One list.

Example
Response · 200 OK
{
  "id": "lst_01j9r5e2g4j6m8p0r2t4w6y8a0",
  "name": "Expo 2026 attendees",
  "description": "Registered through the expo website",
  "created_at": "2026-09-05T08:55:00Z",
  "updated_at": "2026-09-05T09:00:00Z",
  "member_count": 1902,
  "sendable_count": 1840
}
PATCH/lists/{id}recipients:manage

Rename a list or change its description.

Example
Request body
{
  "name": "Expo 2026 attendees (confirmed)"
}
Response · 200 OK
{
  "id": "lst_01j9r5e2g4j6m8p0r2t4w6y8a0",
  "name": "Expo 2026 attendees (confirmed)",
  "description": "Registered through the expo website",
  "created_at": "2026-09-05T08:55:00Z",
  "updated_at": "2026-09-05T09:00:00Z",
  "member_count": 1902,
  "sendable_count": 1840
}
DELETE/lists/{id}recipients:manage

Delete a list. Its recipients stay.

Example

Response · 204 No Content, no body

POST/lists/{id}/membersrecipients:manage

Add recipients to a list.

Example
Request body
{
  "recipient_ids": ["rcp_01j9r5b3d5f7h9k1m3p5r7t9v1"]
}
Response · 200 OK
{
  "requested": 1,
  "added": 1
}
DELETE/lists/{id}/membersrecipients:manage

Remove recipients from a list (they stay in your recipients).

Example
Request body
{
  "recipient_ids": ["rcp_01j9r5b3d5f7h9k1m3p5r7t9v1"]
}
Response · 200 OK
{
  "requested": 1,
  "removed": 1
}

Suppression list#

Telegram IDs and usernames your organization must never message. Opt-outs land here automatically.

GET/suppressionsrecipients:read

Suppressed IDs and usernames.

Query: q, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "sup_01j9rg0j2m4p6r8t0w2y4a6c8e",
      "telegram_id": 443210987,
      "username": null,
      "reason": "Asked by email to stop",
      "source": "MANUAL",
      "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "created_at": "2026-09-12T14:03:00Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/suppressionsrecipients:manage

Suppress a Telegram ID or username, for example after a request by email. Existing recipients who match are opted out at once, and future imports of them arrive opted out.

Example
Request body
{
  "telegram_id": 443210987,
  "reason": "Asked by email to stop"
}
Response · 201 Created
{
  "suppression": {
    "id": "sup_01j9rg0j2m4p6r8t0w2y4a6c8e",
    "telegram_id": 443210987,
    "username": null,
    "reason": "Asked by email to stop",
    "source": "MANUAL",
    "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "created_at": "2026-09-12T14:03:00Z"
  },
  "recipients_opted_out": 1
}
DELETE/suppressions/{id}recipients:manage

Remove a suppression entry. This never restores consent: recipients who opted out stay opted out.

Example

Response · 204 No Content, no body

Templates and attachments#

Reusable message bodies, files to attach, and previews rendered for real recipients.

GET/templatescampaigns:read

Saved message templates.

Query: q, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "tpl_01j9re4g6j8m0p2r4t6w8y0a2c",
      "name": "Event reminder",
      "body": "Hi {{first_name|there}}! {{event_date}} at {{location}}.\n\nStop these messages: {{opt_out_url}}",
      "parse_mode": "MARKDOWN",
      "created_at": "2026-09-10T11:00:00Z",
      "updated_at": "2026-09-10T11:00:00Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/templatescampaigns:manage

Save a template. parse_mode is MARKDOWN, HTML or PLAIN.

Example
Request body
{
  "name": "Event reminder",
  "body": "Hi {{first_name|there}}! {{event_date}} at {{location}}.\n\nStop these messages: {{opt_out_url}}",
  "parse_mode": "MARKDOWN"
}
Response · 201 Created
{
  "id": "tpl_01j9re4g6j8m0p2r4t6w8y0a2c",
  "name": "Event reminder",
  "body": "Hi {{first_name|there}}! {{event_date}} at {{location}}.\n\nStop these messages: {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "created_at": "2026-09-10T11:00:00Z",
  "updated_at": "2026-09-10T11:00:00Z"
}
PATCH/templates/{id}campaigns:manage

Change a template. Campaigns already launched keep the text they were launched with.

Example
Request body
{
  "name": "Event reminder (short)"
}
Response · 200 OK
{
  "id": "tpl_01j9re4g6j8m0p2r4t6w8y0a2c",
  "name": "Event reminder (short)",
  "body": "Hi {{first_name|there}}! {{event_date}} at {{location}}.\n\nStop these messages: {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "created_at": "2026-09-10T11:00:00Z",
  "updated_at": "2026-09-10T11:00:00Z"
}
DELETE/templates/{id}campaigns:manage

Delete a template.

Example

Response · 204 No Content, no body

POST/attachmentscampaigns:manage

Upload a file to attach to a campaign, in a multipart field named file. JPEG, PNG and WebP images become photos (up to 10 MB), video/* becomes a video, anything else a document; 20 MB at most.

Example
Request (multipart/form-data)
curl https://app.example.com/api/v1/attachments \
  -H "Authorization: Bearer $OQIM_API_KEY" \
  -F file=@expo-floor-plan.png
Response · 201 Created
{
  "id": "att_01j9rf7h9k1m3p5r7t9v1x3z5b",
  "kind": "PHOTO",
  "filename": "expo-floor-plan.png",
  "content_type": "image/png",
  "size_bytes": 482113,
  "sha256": "9f2c4e1a7b3d5f60c8e2a4b6d8f0a2c4e6b8d0f2a4c6e8b0d2f4a6c8e0b2d4f6",
  "created_at": "2026-09-28T10:02:00Z"
}
GET/attachments/{id}/contentcampaigns:read

Download an attachment's file, with its original Content-Type.

Example
Response · 200 OK
(the file's bytes)
POST/messages/previewcampaigns:read

Render a message body for one recipient (by default your most recent sendable one): the Telegram HTML, the plain text and its length, the limit, and variables left without a value.

Example
Request body
{
  "body": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}.\n\nNo longer want these messages? {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "recipient_id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1"
}
Response · 200 OK
{
  "html": "Hi Aziza! Tashkent Expo opens tomorrow at 10:00 in Tashkent City Hall.\n\nNo longer want these messages? https://app.example.com/u/b3JnXzAxajlyMm02azRufHJjcF8wMWo5cjVi.q8Zr0mX2pLk5Vd7nWc3TyA",
  "text": "Hi Aziza! Tashkent Expo opens tomorrow at 10:00 in Tashkent City Hall.\n\nNo longer want these messages? https://app.example.com/u/b3JnXzAxajlyMm02azRufHJjcF8wMWo5cjVi.q8Zr0mX2pLk5Vd7nWc3TyA",
  "variables": ["first_name", "location", "opt_out_url"],
  "missing_variables": [],
  "length": 188,
  "limit": 4096,
  "over_limit": false,
  "sample_recipient": {
    "id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
    "first_name": "Aziza",
    "last_name": "Karimova",
    "username": "aziza_k",
    "consent_status": "OPTED_IN"
  }
}

Campaigns#

Create a draft, validate it, launch it, then pause, resume or cancel it. Which fields can change depends on the status; see Campaigns. A repeating campaign runs as a series of campaigns that Oqim adds and launches one at a time; see Scheduling.

GET/campaignscampaigns:read

Campaigns, newest first. status accepts several values separated by commas. A repeating campaign has recurrence and next_run_at; its runs have series_id and occurrence.

Query: status, q, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "name": "Expo reminder, day before",
      "description": "",
      "status": "RUNNING",
      "status_reason": null,
      "message_text": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}.\n\nNo longer want these messages? {{opt_out_url}}",
      "parse_mode": "MARKDOWN",
      "disable_link_preview": true,
      "attachment_id": null,
      "audience": {
        "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
        "tags": [],
        "recipient_ids": []
      },
      "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
      "timezone": "Asia/Tashkent",
      "start_at": "2026-09-30T05:00:00Z",
      "end_at": "2026-09-30T15:00:00Z",
      "window_start": 600,
      "window_end": 1200,
      "window_days": [1, 2, 3, 4, 5],
      "recurrence": null,
      "series_id": null,
      "occurrence": null,
      "auto_launch_at": null,
      "next_run_at": null,
      "total_recipients": 1840,
      "delivered_count": 612,
      "failed_count": 4,
      "unavailable_count": 9,
      "skipped_count": 0,
      "pending_count": 1215,
      "in_flight_count": 3,
      "launched_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "launched_at": "2026-09-29T14:02:11Z",
      "started_at": "2026-09-30T05:00:02Z",
      "paused_at": null,
      "completed_at": null,
      "cancelled_at": null,
      "created_at": "2026-09-28T10:15:00Z",
      "updated_at": "2026-09-30T08:14:10Z"
    }
  ],
  "total": 18,
  "limit": 50,
  "offset": 0
}
POST/campaignscampaigns:manage

Create a draft. Everything except name can be filled in later. Set recurrence to repeat it; see Scheduling.

recurrence: frequency (DAILY, WEEKLY or MONTHLY), interval (every N days up to 60, weeks or months up to 12; default 1), weekdays (WEEKLY: MON to SUN, all sending days), month_day (MONTHLY: 1 to 31; shorter months use their last day) and ends: type NEVER, AFTER with count (2 to 365 runs, the first included) or ON with until (a date in the campaign's time zone, at most 2 years ahead). null means it runs once. Errors come back per field, like recurrence.weekdays.

Example
Request body
{
  "name": "Expo reminder, day before",
  "message_text": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}.\n\nNo longer want these messages? {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "disable_link_preview": true,
  "audience": {
    "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
    "tags": [],
    "recipient_ids": []
  },
  "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
  "timezone": "Asia/Tashkent",
  "start_at": "2026-09-30T05:00:00Z",
  "end_at": "2026-09-30T15:00:00Z",
  "window_start": 600,
  "window_end": 1200,
  "window_days": [1, 2, 3, 4, 5],
  "recurrence": {
    "frequency": "WEEKLY",
    "interval": 1,
    "weekdays": ["MON", "THU"],
    "ends": { "type": "AFTER", "count": 8 }
  }
}
Response · 201 Created
{
  "id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Expo reminder, day before",
  "description": "",
  "status": "DRAFT",
  "status_reason": null,
  "message_text": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}.\n\nNo longer want these messages? {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "disable_link_preview": true,
  "attachment_id": null,
  "audience": {
    "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
    "tags": [],
    "recipient_ids": []
  },
  "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
  "timezone": "Asia/Tashkent",
  "start_at": "2026-09-30T05:00:00Z",
  "end_at": "2026-09-30T15:00:00Z",
  "window_start": 600,
  "window_end": 1200,
  "window_days": [1, 2, 3, 4, 5],
  "recurrence": {
    "frequency": "WEEKLY",
    "interval": 1,
    "weekdays": ["MON", "THU"],
    "ends": { "type": "AFTER", "count": 8 }
  },
  "series_id": null,
  "occurrence": null,
  "auto_launch_at": null,
  "next_run_at": "2026-10-01T05:00:00Z",
  "total_recipients": 0,
  "delivered_count": 0,
  "failed_count": 0,
  "unavailable_count": 0,
  "skipped_count": 0,
  "pending_count": 0,
  "in_flight_count": 0,
  "launched_by": null,
  "launched_at": null,
  "started_at": null,
  "paused_at": null,
  "completed_at": null,
  "cancelled_at": null,
  "created_at": "2026-09-28T10:15:00Z",
  "updated_at": "2026-09-30T08:14:10Z"
}
GET/campaigns/{id}campaigns:read

One campaign with its counters. A run of a repeating campaign also has series: runs launched so far, total_runs when the series ends after a count, whether it's still repeating, the rule, when the next run starts (next_run_at, next_run_id) and its neighbours (previous_id, next_id).

next_run_at on the campaign itself is when the run after it would start, projected from the rule; null when it doesn't repeat. auto_launch_at is set on a run Oqim added: it launches by itself then, 15 minutes before its start.

Example
Response · 200 OK
{
  "id": "cmp_01j9s6g8j0m2p4r6t8w0y2a4c6",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Expo weekly digest · run 2",
  "description": "",
  "status": "SCHEDULED",
  "status_reason": null,
  "message_text": "Hi {{first_name|there}}! This week at Tashkent Expo: {{highlights}}.\n\nNo longer want these messages? {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "disable_link_preview": true,
  "attachment_id": null,
  "audience": {
    "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
    "tags": [],
    "recipient_ids": []
  },
  "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
  "timezone": "Asia/Tashkent",
  "start_at": "2026-10-01T05:00:00Z",
  "end_at": "2026-10-01T13:00:00Z",
  "window_start": 600,
  "window_end": 1080,
  "window_days": [1, 2, 3, 4, 5],
  "recurrence": {
    "frequency": "WEEKLY",
    "interval": 1,
    "weekdays": ["MON", "THU"],
    "ends": { "type": "AFTER", "count": 8 }
  },
  "series_id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
  "occurrence": 2,
  "auto_launch_at": null,
  "next_run_at": "2026-10-05T05:00:00Z",
  "total_recipients": 1840,
  "delivered_count": 0,
  "failed_count": 0,
  "unavailable_count": 0,
  "skipped_count": 0,
  "pending_count": 1840,
  "in_flight_count": 0,
  "launched_by": "system",
  "launched_at": "2026-10-01T04:45:01Z",
  "started_at": null,
  "paused_at": null,
  "completed_at": null,
  "cancelled_at": null,
  "created_at": "2026-09-28T13:00:04Z",
  "updated_at": "2026-10-01T04:45:01Z",
  "series": {
    "id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
    "runs": 2,
    "total_runs": 8,
    "repeating": true,
    "recurrence": {
      "frequency": "WEEKLY",
      "interval": 1,
      "weekdays": ["MON", "THU"],
      "ends": { "type": "AFTER", "count": 8 }
    },
    "next_run_at": "2026-10-01T05:00:00Z",
    "next_run_id": "cmp_01j9s6g8j0m2p4r6t8w0y2a4c6",
    "previous_id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
    "next_id": null
  }
}
PATCH/campaigns/{id}campaigns:manage

Change a campaign. Drafts accept every field; scheduled and paused campaigns only name, description, end_at, the window and account_ids; running ones only name and description. A locked field is 409 field_locked; a finished campaign is 409 campaign_locked.

recurrence changes only while the campaign is a draft; stop a launched series with stop-repeating. Moving the start_at of a run Oqim added moves its auto_launch_at with it.

Example
Request body
{
  "end_at": "2026-09-30T16:00:00Z",
  "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9", "acc_01j9s4e6g8j0m2p4r6t8w0y2a4"]
}
Response · 200 OK
{
  "id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Expo reminder, day before",
  "description": "",
  "status": "RUNNING",
  "status_reason": null,
  "message_text": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}.\n\nNo longer want these messages? {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "disable_link_preview": true,
  "attachment_id": null,
  "audience": {
    "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
    "tags": [],
    "recipient_ids": []
  },
  "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
  "timezone": "Asia/Tashkent",
  "start_at": "2026-09-30T05:00:00Z",
  "end_at": "2026-09-30T16:00:00Z",
  "window_start": 600,
  "window_end": 1200,
  "window_days": [1, 2, 3, 4, 5],
  "recurrence": null,
  "series_id": null,
  "occurrence": null,
  "auto_launch_at": null,
  "next_run_at": null,
  "total_recipients": 1840,
  "delivered_count": 612,
  "failed_count": 4,
  "unavailable_count": 9,
  "skipped_count": 0,
  "pending_count": 1215,
  "in_flight_count": 3,
  "launched_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "launched_at": "2026-09-29T14:02:11Z",
  "started_at": "2026-09-30T05:00:02Z",
  "paused_at": null,
  "completed_at": null,
  "cancelled_at": null,
  "created_at": "2026-09-28T10:15:00Z",
  "updated_at": "2026-09-30T08:14:10Z"
}
DELETE/campaigns/{id}campaigns:manage

Delete a draft. Launched campaigns can't be deleted (409 campaign_not_draft); cancel them instead. Deleting the next run of a repeating campaign ends its series.

Example

Response · 204 No Content, no body

POST/campaigns/{id}/previewcampaigns:read

Render the campaign's message for a recipient (by default your most recent sendable one).

Example
Request body
{
  "recipient_id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1"
}
Response · 200 OK
{
  "html": "Hi Aziza! Tashkent Expo opens tomorrow at 10:00 in Tashkent City Hall.\n\nNo longer want these messages? https://app.example.com/u/b3JnXzAxajlyMm02azRufHJjcF8wMWo5cjVi.q8Zr0mX2pLk5Vd7nWc3TyA",
  "text": "Hi Aziza! Tashkent Expo opens tomorrow at 10:00 in Tashkent City Hall.\n\nNo longer want these messages? https://app.example.com/u/b3JnXzAxajlyMm02azRufHJjcF8wMWo5cjVi.q8Zr0mX2pLk5Vd7nWc3TyA",
  "variables": ["first_name", "location", "opt_out_url"],
  "missing_variables": [],
  "length": 188,
  "limit": 4096,
  "over_limit": false,
  "sample_recipient": {
    "id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
    "first_name": "Aziza",
    "last_name": "Karimova",
    "username": "aziza_k",
    "consent_status": "OPTED_IN"
  }
}
POST/campaigns/{id}/validatecampaigns:read

Run the pre-launch checks without launching. Also saved on the campaign as validation.

Example
Response · 200 OK
{
  "ok": false,
  "checked_at": "2026-09-29T14:01:58Z",
  "estimated_hours": 6.2,
  "audience": {
    "matched": 1902,
    "sendable": 1840,
    "opted_out": 31,
    "suppressed": 6,
    "unknown_consent": 25,
    "missing_variables": 0
  },
  "checks": [
    {
      "key": "organization_active",
      "label": "Organization active",
      "status": "PASS",
      "message": "The organization is active."
    },
    {
      "key": "accounts_authorized",
      "label": "Accounts authorized",
      "status": "FAIL",
      "message": "Expo Desk (needs to sign in again) can't send. Remove it from the campaign or fix it first."
    },
    {
      "key": "unknown_consent",
      "label": "Unknown consent",
      "status": "WARN",
      "message": "25 recipients with unknown consent will be skipped. Record their consent to include them."
    },
    {
      "key": "opt_out_link",
      "label": "Opt-out link",
      "status": "PASS",
      "message": "Recipients can opt out with the included link."
    }
  ]
}
POST/campaigns/{id}/startcampaigns:execute

Launch a draft: re-run the checks, freeze the message and audience, and move to SCHEDULED. A failed check is 422 campaign_invalid with the report in validation; a full plan is 403 plan_limit_reached.

A draft with recurrence needs a start_at and becomes the first run of its series: series_id is its own id and occurrence is 1. Oqim launches the later runs itself through the same checks.

Example
Response · 200 OK
{
  "id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Expo reminder, day before",
  "description": "",
  "status": "SCHEDULED",
  "status_reason": null,
  "message_text": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}.\n\nNo longer want these messages? {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "disable_link_preview": true,
  "attachment_id": null,
  "audience": {
    "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
    "tags": [],
    "recipient_ids": []
  },
  "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
  "timezone": "Asia/Tashkent",
  "start_at": "2026-09-30T05:00:00Z",
  "end_at": "2026-09-30T15:00:00Z",
  "window_start": 600,
  "window_end": 1200,
  "window_days": [1, 2, 3, 4, 5],
  "recurrence": null,
  "series_id": null,
  "occurrence": null,
  "auto_launch_at": null,
  "next_run_at": null,
  "total_recipients": 1840,
  "delivered_count": 0,
  "failed_count": 0,
  "unavailable_count": 0,
  "skipped_count": 0,
  "pending_count": 1840,
  "in_flight_count": 0,
  "launched_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "launched_at": "2026-09-29T14:02:11Z",
  "started_at": null,
  "paused_at": null,
  "completed_at": null,
  "cancelled_at": null,
  "created_at": "2026-09-28T10:15:00Z",
  "updated_at": "2026-09-30T08:14:10Z"
}
POST/campaigns/{id}/pausecampaigns:execute

Stop dispatching a running or scheduled campaign. Recipients keep their place. Pausing a paused campaign returns it unchanged.

Example
Response · 200 OK
{
  "id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Expo reminder, day before",
  "description": "",
  "status": "PAUSED",
  "status_reason": "Paused by dilnoza@silkroad.example",
  "message_text": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}.\n\nNo longer want these messages? {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "disable_link_preview": true,
  "attachment_id": null,
  "audience": {
    "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
    "tags": [],
    "recipient_ids": []
  },
  "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
  "timezone": "Asia/Tashkent",
  "start_at": "2026-09-30T05:00:00Z",
  "end_at": "2026-09-30T15:00:00Z",
  "window_start": 600,
  "window_end": 1200,
  "window_days": [1, 2, 3, 4, 5],
  "recurrence": null,
  "series_id": null,
  "occurrence": null,
  "auto_launch_at": null,
  "next_run_at": null,
  "total_recipients": 1840,
  "delivered_count": 612,
  "failed_count": 4,
  "unavailable_count": 9,
  "skipped_count": 0,
  "pending_count": 1215,
  "in_flight_count": 3,
  "launched_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "launched_at": "2026-09-29T14:02:11Z",
  "started_at": "2026-09-30T05:00:02Z",
  "paused_at": "2026-09-30T08:20:00Z",
  "completed_at": null,
  "cancelled_at": null,
  "created_at": "2026-09-28T10:15:00Z",
  "updated_at": "2026-09-30T08:14:10Z"
}
POST/campaigns/{id}/resumecampaigns:execute

Resume a paused campaign: back to RUNNING if it had started, otherwise SCHEDULED. Refused (409) when its end time has passed (campaign_ended), none of its accounts can send (no_active_accounts) or a platform administrator paused it (paused_by_admin).

Example
Response · 200 OK
{
  "id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Expo reminder, day before",
  "description": "",
  "status": "RUNNING",
  "status_reason": null,
  "message_text": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}.\n\nNo longer want these messages? {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "disable_link_preview": true,
  "attachment_id": null,
  "audience": {
    "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
    "tags": [],
    "recipient_ids": []
  },
  "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
  "timezone": "Asia/Tashkent",
  "start_at": "2026-09-30T05:00:00Z",
  "end_at": "2026-09-30T15:00:00Z",
  "window_start": 600,
  "window_end": 1200,
  "window_days": [1, 2, 3, 4, 5],
  "recurrence": null,
  "series_id": null,
  "occurrence": null,
  "auto_launch_at": null,
  "next_run_at": null,
  "total_recipients": 1840,
  "delivered_count": 612,
  "failed_count": 4,
  "unavailable_count": 9,
  "skipped_count": 0,
  "pending_count": 1215,
  "in_flight_count": 3,
  "launched_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "launched_at": "2026-09-29T14:02:11Z",
  "started_at": "2026-09-30T05:00:02Z",
  "paused_at": null,
  "completed_at": null,
  "cancelled_at": null,
  "created_at": "2026-09-28T10:15:00Z",
  "updated_at": "2026-09-30T08:14:10Z"
}
POST/campaigns/{id}/cancelcampaigns:execute

Stop a draft, scheduled, running or paused campaign for good. Recipients not yet sent to are marked cancelled. Cancelling a run of a repeating campaign also stops its series.

Example
Response · 200 OK
{
  "id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Expo reminder, day before",
  "description": "",
  "status": "CANCELLED",
  "status_reason": null,
  "message_text": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}.\n\nNo longer want these messages? {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "disable_link_preview": true,
  "attachment_id": null,
  "audience": {
    "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
    "tags": [],
    "recipient_ids": []
  },
  "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
  "timezone": "Asia/Tashkent",
  "start_at": "2026-09-30T05:00:00Z",
  "end_at": "2026-09-30T15:00:00Z",
  "window_start": 600,
  "window_end": 1200,
  "window_days": [1, 2, 3, 4, 5],
  "recurrence": null,
  "series_id": null,
  "occurrence": null,
  "auto_launch_at": null,
  "next_run_at": null,
  "total_recipients": 1840,
  "delivered_count": 612,
  "failed_count": 4,
  "unavailable_count": 9,
  "skipped_count": 0,
  "pending_count": 0,
  "in_flight_count": 0,
  "launched_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "launched_at": "2026-09-29T14:02:11Z",
  "started_at": "2026-09-30T05:00:02Z",
  "paused_at": null,
  "completed_at": null,
  "cancelled_at": "2026-09-30T08:25:00Z",
  "created_at": "2026-09-28T10:15:00Z",
  "updated_at": "2026-09-30T08:14:10Z"
}
POST/campaigns/{id}/stop-repeatingcampaigns:execute

Stop a repeating campaign from adding runs. Call it on any run of the series; a run in progress finishes normally. The next run waiting to launch by itself is deleted if nobody edited it, and kept as an ordinary draft otherwise; the run you call it on is always kept. Stopping a stopped series changes nothing; a campaign that never repeated is 409 not_repeating.

Example
Response · 200 OK
{
  "id": "cmp_01j9s6g8j0m2p4r6t8w0y2a4c6",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Expo weekly digest · run 2",
  "description": "",
  "status": "SCHEDULED",
  "status_reason": null,
  "message_text": "Hi {{first_name|there}}! This week at Tashkent Expo: {{highlights}}.\n\nNo longer want these messages? {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "disable_link_preview": true,
  "attachment_id": null,
  "audience": {
    "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
    "tags": [],
    "recipient_ids": []
  },
  "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
  "timezone": "Asia/Tashkent",
  "start_at": "2026-10-01T05:00:00Z",
  "end_at": "2026-10-01T13:00:00Z",
  "window_start": 600,
  "window_end": 1080,
  "window_days": [1, 2, 3, 4, 5],
  "recurrence": null,
  "series_id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
  "occurrence": 2,
  "auto_launch_at": null,
  "next_run_at": null,
  "total_recipients": 1840,
  "delivered_count": 0,
  "failed_count": 0,
  "unavailable_count": 0,
  "skipped_count": 0,
  "pending_count": 1840,
  "in_flight_count": 0,
  "launched_by": "system",
  "launched_at": "2026-10-01T04:45:01Z",
  "started_at": null,
  "paused_at": null,
  "completed_at": null,
  "cancelled_at": null,
  "created_at": "2026-09-28T13:00:04Z",
  "updated_at": "2026-10-01T04:45:01Z",
  "series": {
    "id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
    "runs": 2,
    "total_runs": null,
    "repeating": false,
    "recurrence": null,
    "next_run_at": "2026-10-01T05:00:00Z",
    "next_run_id": "cmp_01j9s6g8j0m2p4r6t8w0y2a4c6",
    "previous_id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
    "next_id": null
  }
}
POST/campaigns/{id}/duplicatecampaigns:manage

Copy a campaign into a new draft with the same message, audience, accounts and schedule. A repeat rule is copied too, as a new series, unless it no longer fits (its end date passed, say).

Example
Response · 201 Created
{
  "id": "cmp_01j9s5f7h9k1m3p5r7t9v1x3z5",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Expo reminder, day before (copy)",
  "description": "",
  "status": "DRAFT",
  "status_reason": null,
  "message_text": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}.\n\nNo longer want these messages? {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "disable_link_preview": true,
  "attachment_id": null,
  "audience": {
    "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
    "tags": [],
    "recipient_ids": []
  },
  "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
  "timezone": "Asia/Tashkent",
  "start_at": "2026-09-30T05:00:00Z",
  "end_at": "2026-09-30T15:00:00Z",
  "window_start": 600,
  "window_end": 1200,
  "window_days": [1, 2, 3, 4, 5],
  "recurrence": null,
  "series_id": null,
  "occurrence": null,
  "auto_launch_at": null,
  "next_run_at": null,
  "total_recipients": 0,
  "delivered_count": 0,
  "failed_count": 0,
  "unavailable_count": 0,
  "skipped_count": 0,
  "pending_count": 0,
  "in_flight_count": 0,
  "launched_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "launched_at": null,
  "started_at": null,
  "paused_at": null,
  "completed_at": null,
  "cancelled_at": null,
  "created_at": "2026-09-28T10:15:00Z",
  "updated_at": "2026-09-30T08:14:10Z"
}
GET/campaigns/{id}/audience-previewcampaigns:read

Who an Instagram or Facebook campaign would reach if it sent now, and why the rest wouldn't. Telegram campaigns answer 409: their audience is their recipient list.

Example
Response · 200 OK
{
  "campaign_id": "cmp_…",
  "channel": "INSTAGRAM | FACEBOOK",
  "account_id": "cha_…",
  "audience": "OPEN_WINDOW | MARKETING_SUBSCRIBERS",
  "reachable": 12,
  "window_closing_within_1h": 3,
  "excluded": {
    "window_closed": 4,
    "opted_out": 1,
    "personal": 2,
    "handed_off": 1,
    "ai_off": 0,
    "cooling_down": 0,
    "stopped": 0,
    "expired": 0
  },
  "by_language": { "uz": 7, "ru": 3, "en": 2, "default": 0 },
  "sendable": true,
  "sendable_reason": "why not, when sendable is false",
  "evaluated_at": "2026-09-28T00:00:00Z"
}
GET/campaigns/{id}/statisticscampaigns:read

Live progress: counts by status, a lane per account with its latest sends, a time series, the current rate and an ETA.

Example
Response · 200 OK
{
  "campaign_id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
  "status": "RUNNING",
  "total": 1840,
  "delivered": 612,
  "failed": 4,
  "unavailable": 9,
  "skipped": 0,
  "pending": 1215,
  "in_flight": 3,
  "by_status": {
    "DELIVERED": 612,
    "FAILED": 4,
    "RECIPIENT_UNAVAILABLE": 9,
    "QUEUED": 2,
    "SENDING": 1,
    "PENDING": 1212
  },
  "by_account": [
    {
      "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "display_name": "Silk Road Support",
      "username": "silkroad_support",
      "status": "ACTIVE",
      "status_reason": null,
      "cooldown_until": null,
      "delivered": 318,
      "failed": 2,
      "unavailable": 5,
      "in_flight": 1,
      "ticks": [
        { "at": "2026-09-30T08:14:02Z", "status": "DELIVERED" },
        { "at": "2026-09-30T08:14:10Z", "status": "DELIVERED" }
      ]
    }
  ],
  "series": [
    {
      "at": "2026-09-30T08:05:00Z",
      "delivered": 38,
      "failed": 0,
      "unavailable": 1
    },
    {
      "at": "2026-09-30T08:10:00Z",
      "delivered": 41,
      "failed": 1,
      "unavailable": 0
    }
  ],
  "bucket_seconds": 300,
  "rate_per_minute": 7.8,
  "eta": "2026-09-30T11:05:00Z"
}
GET/campaigns/{id}/recipientscampaigns:read

Per-recipient delivery state, the account that sent it, attempts and the last error.

Query: status, q, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "crp_01j9rd1f3h5k7m9p1s3v5x7z9b",
      "campaign_id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
      "recipient_id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
      "status": "DELIVERED",
      "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "job_id": "job_01j9rc8e0g2j4m6p8r0t2w4y6a",
      "telegram_message_id": 48213,
      "attempts": 1,
      "last_error": null,
      "queued_at": "2026-09-30T08:14:01Z",
      "sent_at": "2026-09-30T08:14:02Z",
      "updated_at": "2026-09-30T08:14:02Z",
      "recipient_name": "Aziza Karimova",
      "recipient_username": "aziza_k",
      "recipient_telegram_id": 123456789,
      "account_name": "Silk Road Support"
    }
  ],
  "total": 1840,
  "limit": 50,
  "offset": 0
}

Jobs and events#

Individual send jobs, the organization's event history, and a live stream of the same events.

GET/jobscampaigns:read

Send jobs with their result: DELIVERED, FAILED, RECIPIENT_UNAVAILABLE, ACCOUNT_RESTRICTED, AUTH_REQUIRED, TEMPORARY_ERROR or CANCELLED.

Query: campaign_id, account_id, status, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "job_01j9rc8e0g2j4m6p8r0t2w4y6a",
      "campaign_id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
      "campaign_recipient_id": "crp_01j9rd1f3h5k7m9p1s3v5x7z9b",
      "recipient_id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
      "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "status": "SUCCEEDED",
      "result": "DELIVERED",
      "attempts": 1,
      "last_error": null,
      "created_at": "2026-09-30T08:14:01Z",
      "started_at": "2026-09-30T08:14:01Z",
      "completed_at": "2026-09-30T08:14:02Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/eventscampaigns:read

Organization events, newest first: the same events webhooks deliver. type takes several values separated by commas; since takes a time or a date.

Query: type, since, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "evt_01j9r7m2p4s6v8x0z2b4d6f8h0",
      "type": "campaign.started",
      "data": { "campaign_id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4", "total": 1840 },
      "created_at": "2026-09-30T05:00:02Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/events/streamcampaigns:read or ai:read

Server-sent events (text/event-stream): every organization event as it happens, with a ping comment every 20 seconds to keep proxies from closing the connection. Readable with campaigns:read or ai:read (for the AI's events: ai.analysis.updated, ai.insights.narrative_ready, ai.cases.generated, ai.insights.backfill and the rest).

Example
Response · 200 OK
retry: 5000
: connected

id: evt_01j9r7m2p4s6v8x0z2b4d6f8h0
event: campaign.started
data: {"id":"evt_01j9r7m2p4s6v8x0z2b4d6f8h0","type":"campaign.started","data":{"campaign_id":"cmp_01j9r4c6e8g0j2m4p6r8t0w2y4","total":1840},"created_at":"2026-09-30T05:00:02Z"}

: ping

API keys#

Keys act as the API_CLIENT role in one organization. The full key appears once, in the create and rotate responses.

GET/api-keysdevelopers:manage

Keys with their prefix and last use. Secrets are never returned.

Example
Response · 200 OK
{
  "data": [
    {
      "id": "key_01j9r8a1c3e5g7j9m1p3r5t7w9",
      "name": "CRM sync",
      "prefix": "oqk_n4v8t2kq7wzc",
      "last_used_at": null,
      "last_used_ip": null,
      "expires_at": null,
      "revoked_at": null,
      "rotated_from": null,
      "created_at": "2026-09-26T09:30:00Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/api-keysdevelopers:manage

Create a key. expires_in_days is 1 to 3650; leave it out for a key that doesn't expire. The plan must include API access.

Example
Request body
{
  "name": "CRM sync",
  "expires_in_days": 90
}
Response · 201 Created
{
  "id": "key_01j9r8a1c3e5g7j9m1p3r5t7w9",
  "name": "CRM sync",
  "prefix": "oqk_n4v8t2kq7wzc",
  "last_used_at": null,
  "last_used_ip": null,
  "expires_at": "2026-12-25T09:30:00Z",
  "revoked_at": null,
  "rotated_from": null,
  "created_at": "2026-09-26T09:30:00Z",
  "secret": "oqk_n4v8t2kq7wzc_yR3fT9qLm2Vx8KpZc6WnB4sHd1JgQ7eU0aXoN5tYiEw"
}
POST/api-keys/{id}/rotatedevelopers:manage

Issue a replacement key. The old key keeps working for 24 hours, then stops. A revoked or expired key can't be rotated (409 key_inactive).

Example
Response · 201 Created
{
  "id": "key_01j9s7h9k1m3p5r7t9v1x3z5b7",
  "name": "CRM sync",
  "prefix": "oqk_p7s2d9fm3ktx",
  "last_used_at": null,
  "last_used_ip": null,
  "expires_at": null,
  "revoked_at": null,
  "rotated_from": "key_01j9r8a1c3e5g7j9m1p3r5t7w9",
  "created_at": "2026-09-26T09:30:00Z",
  "secret": "oqk_p7s2d9fm3ktx_Lq8Wz3Nc6Vb1Xm4Ks7Rd0Tf2Yh5Jg9Pa3Ue6Io1Bn8C"
}
DELETE/api-keys/{id}developers:manage

Revoke a key immediately, along with any access tokens issued from it.

Example

Response · 204 No Content, no body

Webhooks#

HTTPS endpoints that receive signed events. See Webhooks for payloads and signature verification.

GET/webhookswebhooks:read

Webhook endpoints and their health.

Example
Response · 200 OK
{
  "data": [
    {
      "id": "whk_01j9r6d4f6h8k0m2p4s6v8x0z2",
      "url": "https://hooks.silkroad.example/oqim",
      "description": "CRM sync",
      "events": ["campaign.completed", "recipient.opted_out"],
      "status": "ACTIVE",
      "failure_streak": 0,
      "last_success_at": "2026-09-26T09:12:00Z",
      "last_failure_at": null,
      "created_at": "2026-09-10T08:00:00Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/webhookswebhooks:manage

Add an endpoint for some event types, or ["*"] for all. In production the URL must be https on a public host. The signing secret appears once, in this response.

Example
Request body
{
  "url": "https://hooks.silkroad.example/oqim",
  "description": "CRM sync",
  "events": ["campaign.completed", "recipient.opted_out"]
}
Response · 201 Created
{
  "id": "whk_01j9r6d4f6h8k0m2p4s6v8x0z2",
  "url": "https://hooks.silkroad.example/oqim",
  "description": "CRM sync",
  "events": ["campaign.completed", "recipient.opted_out"],
  "status": "ACTIVE",
  "failure_streak": 0,
  "last_success_at": null,
  "last_failure_at": null,
  "created_at": "2026-09-10T08:00:00Z",
  "signing_secret": "whsec_Qm9vc3RlZC1ieS1vcWltLXdlYmhvb2tz"
}
GET/webhooks/{id}webhooks:read

One endpoint.

Example
Response · 200 OK
{
  "id": "whk_01j9r6d4f6h8k0m2p4s6v8x0z2",
  "url": "https://hooks.silkroad.example/oqim",
  "description": "CRM sync",
  "events": ["campaign.completed", "recipient.opted_out"],
  "status": "ACTIVE",
  "failure_streak": 0,
  "last_success_at": "2026-09-26T09:12:00Z",
  "last_failure_at": null,
  "created_at": "2026-09-10T08:00:00Z"
}
PATCH/webhooks/{id}webhooks:manage

Change the URL, description or events, or set status to ACTIVE to re-enable an endpoint that was disabled after repeated failures.

Example
Request body
{
  "events": ["*"],
  "status": "ACTIVE"
}
Response · 200 OK
{
  "id": "whk_01j9r6d4f6h8k0m2p4s6v8x0z2",
  "url": "https://hooks.silkroad.example/oqim",
  "description": "CRM sync",
  "events": ["*"],
  "status": "ACTIVE",
  "failure_streak": 0,
  "last_success_at": "2026-09-26T09:12:00Z",
  "last_failure_at": null,
  "created_at": "2026-09-10T08:00:00Z"
}
DELETE/webhooks/{id}webhooks:manage

Delete an endpoint and its delivery history.

Example

Response · 204 No Content, no body

POST/webhooks/{id}/testwebhooks:manage

Queue a webhook.test delivery to this endpoint and return it (202). A disabled endpoint is 409 webhook_disabled.

Example
Response · 202 Accepted
{
  "id": "dlv_01j9s9k1m3p5r7t9v1x3z5b7d9",
  "webhook_id": "whk_01j9r6d4f6h8k0m2p4s6v8x0z2",
  "event_id": "evt_01j9r7m2p4s6v8x0z2b4d6f8h0",
  "event_type": "webhook.test",
  "status": "PENDING",
  "attempts": 0,
  "response_status": null,
  "response_body": null,
  "duration_ms": null,
  "created_at": "2026-09-30T13:41:07Z",
  "delivered_at": null
}
POST/webhooks/{id}/rotate-secretwebhooks:manage

Replace the signing secret. Deliveries are signed with the new secret from now on.

Example
Response · 200 OK
{
  "id": "whk_01j9r6d4f6h8k0m2p4s6v8x0z2",
  "url": "https://hooks.silkroad.example/oqim",
  "description": "CRM sync",
  "events": ["campaign.completed", "recipient.opted_out"],
  "status": "ACTIVE",
  "failure_streak": 0,
  "last_success_at": "2026-09-26T09:12:00Z",
  "last_failure_at": null,
  "created_at": "2026-09-10T08:00:00Z",
  "signing_secret": "whsec_TmV3LXNlY3JldC1hZnRlci1yb3RhdGlv"
}
GET/webhooks/{id}/deliverieswebhooks:read

The latest 100 deliveries with their attempts, the response status, the first 2 KB of the response body and the duration.

Example
Response · 200 OK
{
  "data": [
    {
      "id": "dlv_01j9rb5d7f9h1k3m5p7r9t1v3x",
      "webhook_id": "whk_01j9r6d4f6h8k0m2p4s6v8x0z2",
      "event_id": "evt_01j9r7m2p4s6v8x0z2b4d6f8h0",
      "event_type": "campaign.completed",
      "status": "SUCCEEDED",
      "attempts": 1,
      "response_status": 204,
      "response_body": "",
      "duration_ms": 142,
      "created_at": "2026-09-30T13:41:07Z",
      "delivered_at": "2026-09-30T13:41:07Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

AI Seller: configuration#

Configure the AI Seller: the business the AI represents, its offer catalog, sales pipeline, persona, communication style, and runtime settings. Reads require ai:read; writes require ai:manage.

GET/ai/businessesai:read

All businesses in the organization.

Example
Response · 200 OK
{
  "data": [
    {
      "id": "biz_01",
      "name": "Acme Store",
      "timezone": "Asia/Tashkent"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/ai/businessesai:manage

Create a business. A default 9-stage funnel and seller settings are created atomically.

Example
Request body
{
  "name": "Acme Store",
  "timezone": "Asia/Tashkent"
}
Response · 201 Created
{
  "id": "biz_01",
  "name": "Acme Store",
  "timezone": "Asia/Tashkent"
}
GET/ai/businesses/{id}ai:read

One business.

Example
Response · 200 OK
{
  "id": "biz_01",
  "name": "Acme Store"
}
PATCH/ai/businesses/{id}ai:manage

Update any subset of business fields. business_hours are the opening hours the calendar books within, in the business's timezone: an object of days mon to sun, each {open, close} as HH:MM (close may be 24:00) or null for closed (a missing day is closed too). calendar_config are the booking rules (all optional): enabled (true), default_duration_minutes (30), min_duration_minutes (15), max_duration_minutes (120), buffer_minutes (0, kept free around each meeting), min_notice_minutes (60), max_days_ahead (60) and require_customer_confirmation (true; false lets autonomy 5 book without asking the customer to confirm the time first). Unknown days or fields and out-of-range values are refused (422).

Example
Request body
{
  "name": "Acme Store (updated)",
  "business_hours": {
    "mon": { "open": "09:00", "close": "18:00" },
    "sat": { "open": "10:00", "close": "16:00" },
    "sun": null
  },
  "calendar_config": { "default_duration_minutes": 45, "min_notice_minutes": 120 }
}
Response · 200 OK
{
  "id": "biz_01",
  "name": "Acme Store (updated)"
}
DELETE/ai/businesses/{id}ai:manage

Delete a business and all its data (offers, funnels, identities, settings).

Example

Response · 204 No Content, no body

GET/ai/businesses/{id}/offersai:read

Offers for a business, ordered by sales_priority.

Example
Response · 200 OK
{
  "data": [
    {
      "id": "ofr_01",
      "name": "Premium Plan",
      "type": "SUBSCRIPTION",
      "status": "ACTIVE"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/ai/businesses/{id}/offersai:manage

Create an offer. type is required: PRODUCT, SERVICE, COURSE, SUBSCRIPTION, EVENT, PACKAGE or OTHER. currency must be a 3-letter ISO-4217 code (e.g. USD, UZS, RUB).

Example
Request body
{
  "type": "PRODUCT",
  "name": "Starter Kit",
  "price": 29.99,
  "currency": "USD"
}
Response · 201 Created
{
  "id": "ofr_01",
  "type": "PRODUCT",
  "name": "Starter Kit"
}
PATCH/ai/offers/{id}ai:manage

Update offer fields. status: ACTIVE or ARCHIVED.

Example
Request body
{
  "status": "ARCHIVED"
}
Response · 200 OK
{
  "id": "ofr_01",
  "status": "ARCHIVED"
}
DELETE/ai/offers/{id}ai:manage

Delete an offer.

Example

Response · 204 No Content, no body

GET/ai/businesses/{id}/funnelsai:read

Sales funnels for a business, each with its stages.

Example
Response · 200 OK
{
  "data": [
    {
      "id": "fnl_01",
      "name": "Default",
      "is_default": true,
      "stages": []
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/ai/businesses/{id}/funnelsai:manage

Create a new funnel (empty stages; use PUT /stages to fill it).

Example
Request body
{
  "name": "Warm Leads"
}
Response · 201 Created
{
  "id": "fnl_01",
  "name": "Warm Leads",
  "is_default": false,
  "stages": []
}
GET/ai/funnels/{id}ai:read

One funnel with its stages.

Example
Response · 200 OK
{
  "id": "fnl_01",
  "name": "Default",
  "stages": [
    { "key": "NEW", "name": "New Lead", "position": 0 }
  ]
}
PATCH/ai/funnels/{id}ai:manage

Rename a funnel or change is_default.

Example
Request body
{
  "name": "Default (renamed)"
}
Response · 200 OK
{
  "id": "fnl_01",
  "name": "Default (renamed)"
}
DELETE/ai/funnels/{id}ai:manage

Delete a funnel and its stages.

Example

Response · 204 No Content, no body

PUT/ai/funnels/{id}/stagesai:manage

Replace the entire stage list atomically. stages[] must have unique keys and end with a terminal stage (key CONVERTED, or ending in _DONE, _COMPLETE, _CONVERTED, _CLOSED). Each stage may set objective, allowed_actions and prohibited_actions (the AI Seller only does what the stage allows, and never what it prohibits), recommended_questions, and handoff_rules shaped like the seller settings' ones, which apply on top of them while a conversation is in the stage. Stage ids change with every replace; conversations keep their stage by key.

Example
Request body
{
  "stages": [
    { "key": "NEW", "name": "New Lead" },
    { "key": "CONVERTED", "name": "Converted" }
  ]
}
Response · 200 OK
{
  "funnel_id": "fnl_01",
  "stages": [
    { "key": "NEW", "position": 0 },
    { "key": "CONVERTED", "position": 1 }
  ]
}
GET/ai/businesses/{id}/identitiesai:read

AI personas for a business.

Example
Response · 200 OK
{
  "data": [
    { "id": "idn_01", "display_name": "Zulfiya", "language": "uz" }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/ai/businesses/{id}/identitiesai:manage

Create a persona. display_name is required. biography must not claim the AI is a human (422 if it contains phrases like 'I am a real person', 'я живой человек' or 'men haqiqiy odamman').

Example
Request body
{
  "display_name": "Zulfiya",
  "language": "uz",
  "role": "Sales consultant"
}
Response · 201 Created
{
  "id": "idn_01",
  "display_name": "Zulfiya"
}
PATCH/ai/identities/{id}ai:manage

Update identity fields.

Example
Request body
{
  "display_name": "Zulfiya (updated)"
}
Response · 200 OK
{
  "id": "idn_01",
  "display_name": "Zulfiya (updated)"
}
DELETE/ai/identities/{id}ai:manage

Delete an identity.

Example

Response · 204 No Content, no body

GET/ai/communication-profilesai:read

Built-in profiles (is_builtin: true) and organization-owned profiles. Built-in profiles are read-only; copy them to customize.

Example
Response · 200 OK
{
  "data": [
    {
      "id": "cpf_small_business",
      "name": "Small Business",
      "is_builtin": true
    },
    { "id": "cpf_01", "name": "My Profile", "is_builtin": false }
  ],
  "total": 2,
  "limit": 50,
  "offset": 0
}
POST/ai/communication-profilesai:manage

Create a custom communication profile. All style fields are required: tone, formality, message_length, vocabulary, sentence_structure, greeting_style, emoji_policy, technical_depth, sales_aggressiveness, question_frequency, explanation_depth, cta_style, objection_style.

Example
Request body
{
  "name": "Direct B2B",
  "audience_type": "B2B",
  "language": "en",
  "tone": "PROFESSIONAL",
  "formality": "FORMAL",
  "message_length": "SHORT",
  "vocabulary": "STANDARD",
  "sentence_structure": "SHORT",
  "greeting_style": "FORMAL",
  "emoji_policy": "NONE",
  "technical_depth": "BASIC",
  "sales_aggressiveness": "MODERATE",
  "question_frequency": "LOW",
  "explanation_depth": "BRIEF",
  "cta_style": "DIRECT",
  "objection_style": "EVIDENCE"
}
Response · 201 Created
{
  "id": "cpf_01",
  "name": "Direct B2B"
}
PATCH/ai/communication-profiles/{id}ai:manage

Update a custom profile. 403 if is_builtin.

Example
Request body
{
  "tone": "FRIENDLY"
}
Response · 200 OK
{
  "id": "cpf_01",
  "tone": "FRIENDLY"
}
DELETE/ai/communication-profiles/{id}ai:manage

Delete a custom profile. 403 if is_builtin.

Example

Response · 204 No Content, no body

POST/ai/communication-profiles/{id}/copyai:manage

Copy any profile (built-in or custom) into the organization as a new custom profile.

Example
Request body
{
  "name": "My Custom Profile"
}
Response · 201 Created
{
  "id": "cpf_02",
  "name": "My Custom Profile",
  "is_builtin": false
}
GET/ai/language-profilesai:read

Built-in language profiles (is_builtin: true) and organization-owned ones.

Example
Response · 200 OK
{
  "data": [
    { "id": "lpf_uz_latin", "language": "uz", "is_builtin": true },
    { "id": "lpf_01", "language": "uz", "is_builtin": false }
  ],
  "total": 2,
  "limit": 50,
  "offset": 0
}
POST/ai/language-profilesai:manage

Create a custom language profile. language (uz/ru/en), formality (FORMAL/INFORMAL/MIXED) and vocabulary (SIMPLE/STANDARD/RICH) are required.

Example
Request body
{
  "language": "uz",
  "formality": "INFORMAL",
  "vocabulary": "STANDARD"
}
Response · 201 Created
{
  "id": "lpf_01",
  "language": "uz"
}
PATCH/ai/language-profiles/{id}ai:manage

Update a custom language profile. 403 if is_builtin.

Example
Request body
{
  "formality": "FORMAL"
}
Response · 200 OK
{
  "id": "lpf_01",
  "formality": "FORMAL"
}
DELETE/ai/language-profiles/{id}ai:manage

Delete a custom language profile. 403 if is_builtin.

Example

Response · 204 No Content, no body

POST/ai/language-profiles/{id}/copyai:manage

Copy any language profile into the organization.

Example
Response · 201 Created
{
  "id": "lpf_02",
  "language": "uz",
  "is_builtin": false
}
GET/ai/businesses/{id}/seller-settingsai:read

Runtime settings for the AI Seller on this business.

Example
Response · 200 OK
{
  "business_id": "biz_01",
  "autonomy_level": 2,
  "reply_window_hours": 24,
  "followup_limit": 1,
  "disclosure_enabled": true,
  "active": false
}
PUT/ai/businesses/{id}/seller-settingsai:manage

Create or replace seller settings. autonomy_level 0–5 (0 analytics only, 1 recommendations, 2 drafts a person approves, 3 automatic only for approved_scenarios, 4 automatic, 5 automatic with the tools approved in the agent configuration); reply_window_hours 1–168; followup_limit 0–5. When disclosure_enabled is true, disclosure_text must have non-empty text for uz, ru and en; the first AI message of each conversation starts with it. approved_scenarios for autonomy 3: GREETING, FAQ, PRICING, PRODUCT_INFO, AVAILABILITY, DELIVERY, OBJECTION, CLOSING, SCHEDULING, AI_DISCLOSURE, OTHER (complaints and requests for a person always go to a person). handoff_rules (all optional; a funnel stage's own handoff_rules apply on top while the conversation is in it): triggers turns HUMAN_REQUESTED, COMPLAINT, SENSITIVE_TOPIC, PRICING_EXCEPTION, CALENDAR_AMBIGUITY, POLICY_UNCERTAINTY, LOW_CONFIDENCE, REPEATED_FAILURES on or off (all on by default); keywords hand off (CUSTOM) when the customer's message contains one (a trailing * matches word endings); high_value_min hands off a lead worth at least that (HIGH_VALUE_LEAD, once per conversation); low_confidence_below (default 0.3) and max_failures (default 2) tune LOW_CONFIDENCE and REPEATED_FAILURES. Unknown scenarios, triggers or fields are refused (422).

Example
Request body
{
  "autonomy_level": 3,
  "reply_window_hours": 24,
  "active": true,
  "approved_scenarios": ["FAQ", "PRICING"],
  "handoff_rules": {
    "triggers": { "CALENDAR_AMBIGUITY": false },
    "keywords": ["advokat", "возврат*"],
    "high_value_min": 5000000
  }
}
Response · 200 OK
{
  "business_id": "biz_01",
  "autonomy_level": 3,
  "active": true,
  "approved_scenarios": ["FAQ", "PRICING"]
}
GET/ai/businesses/{id}/accountsai:read

List Telegram accounts mapped to this business. An account can serve only one business.

Example
Response · 200 OK
[
  {
    "organization_id": "org_01",
    "business_id": "biz_01",
    "account_id": "acc_01",
    "created_at": "2026-01-01T00:00:00Z"
  }
]
PUT/ai/businesses/{id}/accountsai:manage

Replace the full set of accounts mapped to this business. Pass an empty array to clear all mappings. Each account must belong to the same organization and cannot be mapped to another business.

Example
Request body
{
  "account_ids": ["acc_01"]
}
Response · 200 OK
[
  {
    "organization_id": "org_01",
    "business_id": "biz_01",
    "account_id": "acc_01",
    "created_at": "2026-01-01T00:00:00Z"
  }
]

AI#

Model providers for the writing assistant, and the assistant itself. Keys are encrypted and never returned. Only the brief or the message text you submit goes to a provider, never recipient data. See AI writing assistant.

GET/ai/catalogorg:read

The built-in providers with their fixed endpoints and where to get a key, plus CUSTOM. allow_private_endpoints says whether custom endpoints may use private network addresses on this installation.

Example
Response · 200 OK
{
  "data": [
    {
      "kind": "OPENAI",
      "name": "OpenAI",
      "base_url": "https://api.openai.com/v1",
      "api_format": "OPENAI",
      "key_url": "https://platform.openai.com/api-keys",
      "key_required": true,
      "model_placeholder": "gpt-5.6"
    },
    {
      "kind": "ANTHROPIC",
      "name": "Anthropic",
      "base_url": "https://api.anthropic.com/v1",
      "api_format": "ANTHROPIC",
      "key_url": "https://platform.claude.com/settings/keys",
      "key_required": true,
      "model_placeholder": "claude-sonnet-5"
    },
    {
      "kind": "CUSTOM",
      "name": "Custom",
      "base_url": "",
      "api_format": "OPENAI",
      "key_url": "",
      "key_required": false,
      "model_placeholder": "llama3.2"
    }
  ],
  "allow_private_endpoints": false
}
GET/ai/providersorg:read

Connected providers, the default first. The API key is never returned; key_hint shows its last characters and endpoint where requests go.

Example
Response · 200 OK
{
  "data": [
    {
      "id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "name": "OpenAI",
      "kind": "OPENAI",
      "api_format": "OPENAI",
      "base_url": null,
      "model": "gpt-5.6",
      "key_hint": "•••• 4f2a",
      "has_key": true,
      "is_default": true,
      "status": "OK",
      "last_error": null,
      "last_checked_at": "2026-09-26T09:30:00Z",
      "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "created_at": "2026-09-20T08:00:00Z",
      "updated_at": "2026-09-26T09:30:00Z",
      "endpoint": "https://api.openai.com/v1"
    },
    {
      "id": "aip_01j9rq1v3x5z7b9d1f3h5k7m9p",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "name": "Office GPU server",
      "kind": "CUSTOM",
      "api_format": "OPENAI",
      "base_url": "http://host.docker.internal:11434/v1",
      "model": "qwen3:8b",
      "key_hint": null,
      "has_key": false,
      "is_default": false,
      "status": "OK",
      "last_error": null,
      "last_checked_at": "2026-09-26T09:30:00Z",
      "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "created_at": "2026-09-20T08:00:00Z",
      "updated_at": "2026-09-26T09:30:00Z",
      "endpoint": "http://host.docker.internal:11434/v1"
    }
  ],
  "total": 2,
  "limit": 50,
  "offset": 0
}
POST/ai/providersorg:manage

Connect a provider. Presets (OPENAI, ANTHROPIC, GEMINI, DEEPSEEK, OPENROUTER) need api_key and use their built-in endpoint. CUSTOM needs base_url, the part before /chat/completions, and api_format OPENAI or ANTHROPIC; its key is optional. The first provider becomes the default; up to 20 per organization. Test it next.

Example
Request body
{
  "kind": "OPENAI",
  "name": "OpenAI",
  "api_key": "sk-…",
  "model": "gpt-5.6",
  "is_default": true
}
Response · 201 Created
{
  "id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "OpenAI",
  "kind": "OPENAI",
  "api_format": "OPENAI",
  "base_url": null,
  "model": "gpt-5.6",
  "key_hint": "•••• 4f2a",
  "has_key": true,
  "is_default": true,
  "status": "UNTESTED",
  "last_error": null,
  "last_checked_at": null,
  "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "created_at": "2026-09-20T08:00:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "endpoint": "https://api.openai.com/v1"
}
PATCH/ai/providers/{id}org:manage

Change a provider. Omit api_key (or send an empty string) to keep the stored key, send a new one to rotate it, or null to remove it from a custom endpoint. Changing the provider or base URL needs the key again. is_default: true makes it the default; to change the default, make another provider the default. Connection changes reset status to UNTESTED.

Example
Request body
{
  "api_key": "sk-…"
}
Response · 200 OK
{
  "id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "OpenAI",
  "kind": "OPENAI",
  "api_format": "OPENAI",
  "base_url": null,
  "model": "gpt-5.6",
  "key_hint": "•••• 9c1d",
  "has_key": true,
  "is_default": true,
  "status": "UNTESTED",
  "last_error": null,
  "last_checked_at": null,
  "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "created_at": "2026-09-20T08:00:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "endpoint": "https://api.openai.com/v1"
}
DELETE/ai/providers/{id}org:manage

Remove a provider and delete its key. When it was the default, the oldest remaining provider becomes the default.

Example

Response · 204 No Content, no body

POST/ai/providers/{id}/testorg:manage

Send a tiny request and record the outcome in status (OK or ERROR), last_error and last_checked_at. A failed test is still a 200 with status ERROR. Counts toward the provider-check limit.

Example
Response · 200 OK
{
  "id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "OpenAI",
  "kind": "OPENAI",
  "api_format": "OPENAI",
  "base_url": null,
  "model": "gpt-5.6",
  "key_hint": "•••• 4f2a",
  "has_key": true,
  "is_default": true,
  "status": "OK",
  "last_error": null,
  "last_checked_at": "2026-09-26T09:30:00Z",
  "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "created_at": "2026-09-20T08:00:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "endpoint": "https://api.openai.com/v1"
}
GET/ai/providers/{id}/modelsorg:manage

The provider's chat models, listed with the stored key. Lists that include every kind of model (OpenAI, Gemini, OpenRouter) are filtered to chat models.

Example
Response · 200 OK
{
  "data": [
    { "id": "gpt-5.2" },
    { "id": "gpt-5.6" }
  ],
  "total": 2,
  "limit": 50,
  "offset": 0
}
POST/ai/modelsorg:manage

List models for a configuration that isn't saved yet, so a form can offer a model picker. The key is used for this one call and is neither stored nor logged.

Example
Request body
{
  "kind": "CUSTOM",
  "api_format": "OPENAI",
  "base_url": "http://host.docker.internal:11434/v1",
  "api_key": ""
}
Response · 200 OK
{
  "data": [
    { "id": "llama3.2:3b" },
    { "id": "qwen3:8b" }
  ],
  "total": 2,
  "limit": 50,
  "offset": 0
}
POST/ai/composecampaigns:manage

Draft or edit one message with a connected provider: provider_id, or the default. action DRAFT needs brief (up to 2,000 characters); IMPROVE, SHORTEN, FRIENDLIER, FORMAL, FIX and TRANSLATE need text (up to 4,096), and TRANSLATE needs language en, ru or uz. The result keeps {{variables}}, follows parse_mode and is never sent to anyone. warnings flags dropped or added placeholders and overlong results.

30 requests an hour per person and 100 per organization (429 ai_rate_limited). A declined request is 422 ai_refused; a provider failure is 502 with an ai_provider_ code. While a platform administrator has AI turned off, it's 403 ai_disabled.

Example
Request body
{
  "action": "IMPROVE",
  "text": "hi {{first_name|there}}, expo opens tomorrow 10:00 in {{location}}. stop: {{opt_out_url}}",
  "parse_mode": "MARKDOWN"
}
Response · 200 OK
{
  "text": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}. See you there.\n\nNo longer want these messages? {{opt_out_url}}",
  "provider_id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
  "model": "gpt-5.6-2026-09-01",
  "usage": { "input_tokens": 412, "output_tokens": 61 },
  "warnings": []
}

AI engine#

The AI Seller's engine: which provider and model each agent uses (with fallbacks), the model registry and its prices, versioned prompts, execution traces, costs and budgets. Model calls go through a router that tries the agent's chain in order, moves on after timeouts, 5xx answers, rate limits and unreachable endpoints (never after a rejected key or a bad request), and skips a provider that keeps failing for a short cool-off. Costs are in US dollars, over UTC days. Every change is written to the audit log.

GET/ai/agentsai:read

The agents (SELLER, CI, CALENDAR, RESEARCH, JUDGE, EMBEDDER), each with whether the platform lets it run (enabled), the tools it can be granted, the organization's configuration (config, null when it uses the default provider), how many businesses override it, and the effective chain with each entry's health.

Example
Response · 200 OK
{
  "data": [
    {
      "key": "SELLER",
      "name": "Seller",
      "description": "Answers customers in conversations they started, with a structured reply and actions that are validated before anything is sent.",
      "capability": "STRUCTURED",
      "enabled": true,
      "tools": [
        {
          "key": "knowledge.search",
          "name": "Search knowledge",
          "description": "Look up the business's knowledge base.",
          "default": true
        },
        {
          "key": "memory.read",
          "name": "Read memory",
          "description": "Read what's remembered about the customer.",
          "default": true
        },
        {
          "key": "handoff.request",
          "name": "Hand over to a person",
          "description": "Pause the AI in the conversation and ask a person to take over.",
          "default": true
        },
        {
          "key": "lead.update",
          "name": "Update the lead",
          "description": "Update the lead's stage, quality and details.",
          "default": true
        },
        {
          "key": "calendar.check_availability",
          "name": "Check availability",
          "description": "Look up free times in the connected calendar.",
          "default": true
        }
      ],
      "config": {
        "id": "agc_01j9rt7x9z1b3d5f7h9k1m3p5s",
        "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
        "business_id": null,
        "agent": "SELLER",
        "provider_id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
        "model": "gpt-5.6",
        "fallbacks": [
          {
            "provider_id": "aip_01j9rq1v3x5z7b9d1f3h5k7m9p",
            "model": "qwen3:8b"
          }
        ],
        "params": { "temperature": 0.4, "max_tokens": 1200 },
        "tool_permissions": [
          "knowledge.search",
          "memory.read",
          "handoff.request",
          "lead.update"
        ],
        "updated_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
        "created_at": "2026-09-21T09:00:00Z",
        "updated_at": "2026-09-26T09:30:00Z"
      },
      "business_overrides": 1,
      "effective": {
        "source": "ORGANIZATION",
        "chain": [
          {
            "role": "PRIMARY",
            "provider_id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
            "provider_name": "OpenAI",
            "provider_kind": "OPENAI",
            "model": "gpt-5.6",
            "health": { "status": "OK", "failures": 0, "until": null }
          },
          {
            "role": "FALLBACK",
            "provider_id": "aip_01j9rq1v3x5z7b9d1f3h5k7m9p",
            "provider_name": "Office GPU server",
            "provider_kind": "CUSTOM",
            "model": "qwen3:8b",
            "health": {
              "status": "COOLING",
              "failures": 3,
              "until": "2026-09-26T09:30:30Z",
              "last_error_code": "timeout"
            }
          }
        ]
      }
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/ai/agents/{agent}/configai:read

An agent's configuration for the organization, or with business_id for one business. config is the row of exactly that scope (null and inherited true when it inherits: a business from the organization, the organization from its default provider). effective is the chain the router uses; an entry with problem (a removed provider, no embedding model) is skipped.

Query: business_id

Example
Response · 200 OK
{
  "agent": {
    "key": "SELLER",
    "name": "Seller",
    "description": "Answers customers in conversations they started, with a structured reply and actions that are validated before anything is sent.",
    "capability": "STRUCTURED"
  },
  "business_id": null,
  "config": {
    "id": "agc_01j9rt7x9z1b3d5f7h9k1m3p5s",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "business_id": null,
    "agent": "SELLER",
    "provider_id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
    "model": "gpt-5.6",
    "fallbacks": [
      {
        "provider_id": "aip_01j9rq1v3x5z7b9d1f3h5k7m9p",
        "model": "qwen3:8b"
      }
    ],
    "params": { "temperature": 0.4, "max_tokens": 1200 },
    "tool_permissions": [
      "knowledge.search",
      "memory.read",
      "handoff.request",
      "lead.update"
    ],
    "updated_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "created_at": "2026-09-21T09:00:00Z",
    "updated_at": "2026-09-26T09:30:00Z"
  },
  "inherited": false,
  "effective": {
    "source": "ORGANIZATION",
    "chain": [
      {
        "role": "PRIMARY",
        "provider_id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
        "provider_name": "OpenAI",
        "provider_kind": "OPENAI",
        "model": "gpt-5.6",
        "health": { "status": "OK", "failures": 0, "until": null }
      },
      {
        "role": "FALLBACK",
        "provider_id": "aip_01j9rq1v3x5z7b9d1f3h5k7m9p",
        "provider_name": "Office GPU server",
        "provider_kind": "CUSTOM",
        "model": "qwen3:8b",
        "health": {
          "status": "COOLING",
          "failures": 3,
          "until": "2026-09-26T09:30:30Z",
          "last_error_code": "timeout"
        }
      }
    ]
  },
  "params": { "temperature": 0.4, "max_tokens": 1200 },
  "tool_permissions": ["knowledge.search", "memory.read", "handoff.request", "lead.update"],
  "tools": [
    {
      "key": "knowledge.search",
      "name": "Search knowledge",
      "description": "Look up the business's knowledge base.",
      "default": true
    },
    {
      "key": "memory.read",
      "name": "Read memory",
      "description": "Read what's remembered about the customer.",
      "default": true
    },
    {
      "key": "handoff.request",
      "name": "Hand over to a person",
      "description": "Pause the AI in the conversation and ask a person to take over.",
      "default": true
    },
    {
      "key": "lead.update",
      "name": "Update the lead",
      "description": "Update the lead's stage, quality and details.",
      "default": true
    },
    {
      "key": "calendar.check_availability",
      "name": "Check availability",
      "description": "Look up free times in the connected calendar.",
      "default": true
    }
  ]
}
PATCH/ai/agents/{agent}/configai:manage

Set which provider and model an agent uses, for the organization or (business_id) one business. Fields left out keep their value. provider_id null is the organization's default provider; model null is the provider's own model (for EMBEDDER, the registry's embedding model for that provider). fallbacks: up to 4 {provider_id, model}, tried in order after retryable failures. params: temperature 0 to 2 (used for every call) and max_tokens 16 to 200,000 (a ceiling per call). tool_permissions: tools from the agent's list. A business's new configuration starts as a copy of the organization's; the organization's starts with the default tools.

Example
Request body
{
  "provider_id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
  "model": "gpt-5.6",
  "fallbacks": [
    {
      "provider_id": "aip_01j9rq1v3x5z7b9d1f3h5k7m9p",
      "model": "qwen3:8b"
    }
  ],
  "params": { "temperature": 0.4, "max_tokens": 1200 }
}
Response · 200 OK
{
  "agent": {
    "key": "SELLER",
    "name": "Seller",
    "description": "Answers customers in conversations they started, with a structured reply and actions that are validated before anything is sent.",
    "capability": "STRUCTURED"
  },
  "business_id": null,
  "config": {
    "id": "agc_01j9rt7x9z1b3d5f7h9k1m3p5s",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "business_id": null,
    "agent": "SELLER",
    "provider_id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
    "model": "gpt-5.6",
    "fallbacks": [
      {
        "provider_id": "aip_01j9rq1v3x5z7b9d1f3h5k7m9p",
        "model": "qwen3:8b"
      }
    ],
    "params": { "temperature": 0.4, "max_tokens": 1200 },
    "tool_permissions": [
      "knowledge.search",
      "memory.read",
      "handoff.request",
      "lead.update"
    ],
    "updated_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "created_at": "2026-09-21T09:00:00Z",
    "updated_at": "2026-09-26T09:30:00Z"
  },
  "inherited": false,
  "effective": {
    "source": "ORGANIZATION",
    "chain": [
      {
        "role": "PRIMARY",
        "provider_id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
        "provider_name": "OpenAI",
        "provider_kind": "OPENAI",
        "model": "gpt-5.6",
        "health": { "status": "OK", "failures": 0, "until": null }
      },
      {
        "role": "FALLBACK",
        "provider_id": "aip_01j9rq1v3x5z7b9d1f3h5k7m9p",
        "provider_name": "Office GPU server",
        "provider_kind": "CUSTOM",
        "model": "qwen3:8b",
        "health": {
          "status": "COOLING",
          "failures": 3,
          "until": "2026-09-26T09:30:30Z",
          "last_error_code": "timeout"
        }
      }
    ]
  },
  "params": { "temperature": 0.4, "max_tokens": 1200 },
  "tool_permissions": ["knowledge.search", "memory.read", "handoff.request", "lead.update"],
  "tools": [
    {
      "key": "knowledge.search",
      "name": "Search knowledge",
      "description": "Look up the business's knowledge base.",
      "default": true
    },
    {
      "key": "memory.read",
      "name": "Read memory",
      "description": "Read what's remembered about the customer.",
      "default": true
    },
    {
      "key": "handoff.request",
      "name": "Hand over to a person",
      "description": "Pause the AI in the conversation and ask a person to take over.",
      "default": true
    },
    {
      "key": "lead.update",
      "name": "Update the lead",
      "description": "Update the lead's stage, quality and details.",
      "default": true
    },
    {
      "key": "calendar.check_availability",
      "name": "Check availability",
      "description": "Look up free times in the connected calendar.",
      "default": true
    }
  ]
}
DELETE/ai/agents/{agent}/configai:manage

Remove the configuration of one scope: a business then inherits the organization's, and the organization's agent uses its default provider.

Query: business_id

Example

Response · 204 No Content, no body

GET/ai/modelsai:read

The model registry as your organization sees it: models with their capabilities, context window and prices in USD per million tokens. Platform entries are kept by Oqim's administrators; an organization entry overrides the platform's for the same provider and model. priced is false when a price the model needs is missing: its calls are then recorded at cost 0 and marked unpriced.

Query: provider_kind, capability

Example
Response · 200 OK
{
  "data": [
    {
      "id": "aim_01j9rs4w6y8a0c2e4g6j8m0p2r",
      "organization_id": null,
      "provider_kind": "OPENAI",
      "model_id": "gpt-5.6",
      "display_name": "GPT-5.6",
      "capabilities": ["CHAT", "STRUCTURED"],
      "context_window": 400000,
      "price_in_per_mtok": 1.25,
      "price_out_per_mtok": 10,
      "price_embed_per_mtok": null,
      "enabled": true,
      "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "updated_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "created_at": "2026-09-20T08:00:00Z",
      "updated_at": "2026-09-26T09:30:00Z",
      "scope": "PLATFORM",
      "priced": true
    },
    {
      "id": "aim_01j9s5ck4m6p8r0t2w4y6a8c0e",
      "organization_id": null,
      "provider_kind": "OPENAI",
      "model_id": "text-embedding-3-small",
      "display_name": null,
      "capabilities": ["EMBEDDINGS"],
      "context_window": 8191,
      "price_in_per_mtok": null,
      "price_out_per_mtok": null,
      "price_embed_per_mtok": 0.02,
      "enabled": true,
      "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "updated_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "created_at": "2026-09-20T08:00:00Z",
      "updated_at": "2026-09-26T09:30:00Z",
      "scope": "PLATFORM",
      "priced": true
    }
  ],
  "total": 2,
  "limit": 50,
  "offset": 0
}
GET/ai/promptsai:read

The prompts your organization sees: Oqim's platform defaults (scope PLATFORM, not editable) and your own, per organization or business, each with its active version. effective names the version each agent uses (for business_id, or the organization): the business's active version, else the organization's, else the platform default. placeholders lists the {{placeholders}} a prompt may use.

Query: agent, business_id

Example
Response · 200 OK
{
  "data": [
    {
      "id": "prm_01j9ry9d1f3h5k7m9p1s3v5x7z",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "business_id": null,
      "agent": "SELLER",
      "name": "Seller, expo season",
      "description": "Shorter replies for the expo weeks.",
      "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "created_at": "2026-09-22T11:00:00Z",
      "updated_at": "2026-09-22T11:00:00Z",
      "scope": "ORGANIZATION",
      "editable": true,
      "versions": 3,
      "active_version": {
        "id": "prv_01j9rz2e4g6j8m0p2r4t6w8y0a",
        "prompt_id": "prm_01j9ry9d1f3h5k7m9p1s3v5x7z",
        "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
        "business_id": null,
        "agent": "SELLER",
        "version": 3,
        "content": "You are {{identity}}\n\nYou write the replies of a business in a Telegram conversation that the customer started. …\n\n# Knowledge\n{{knowledge}}",
        "status": "ACTIVE",
        "changelog": "Shorter answers; always offer a person for refunds.",
        "author_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
        "author_label": "dilnoza@silkroad.example",
        "activated_at": "2026-09-26T09:30:00Z",
        "created_at": "2026-09-25T16:20:00Z",
        "updated_at": "2026-09-26T09:30:00Z"
      }
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0,
  "effective": {
    "SELLER": {
      "id": "prv_01j9rz2e4g6j8m0p2r4t6w8y0a",
      "prompt_id": "prm_01j9ry9d1f3h5k7m9p1s3v5x7z",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "business_id": null,
      "agent": "SELLER",
      "version": 3,
      "content": "",
      "status": "ACTIVE",
      "changelog": "Shorter answers; always offer a person for refunds.",
      "author_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "author_label": "dilnoza@silkroad.example",
      "activated_at": "2026-09-26T09:30:00Z",
      "created_at": "2026-09-25T16:20:00Z",
      "updated_at": "2026-09-26T09:30:00Z",
      "scope": "ORGANIZATION"
    }
  },
  "placeholders": [
    {
      "name": "identity",
      "description": "Who the assistant is: the persona's name, that it's the business's AI assistant, and the disclosure instruction."
    },
    {
      "name": "knowledge",
      "description": "Retrieved knowledge passages with their sources, or a note that none matched."
    }
  ]
}
POST/ai/promptsai:manage

Create a prompt for an agent, for the organization or (business_id) one business, with its first version. Without content it starts as a copy of the version the agent uses now. status is DRAFT (default) or TESTING; activate: true makes it the active version at once. Content is up to 32,000 characters and may use only the listed {{placeholders}}.

Example
Request body
{
  "agent": "SELLER",
  "name": "Seller, expo season",
  "description": "Shorter replies for the expo weeks.",
  "content": "You are {{identity}} …",
  "changelog": "First version.",
  "activate": false
}
Response · 201 Created
{
  "prompt": {
    "id": "prm_01j9ry9d1f3h5k7m9p1s3v5x7z",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "business_id": null,
    "agent": "SELLER",
    "name": "Seller, expo season",
    "description": "Shorter replies for the expo weeks.",
    "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "created_at": "2026-09-22T11:00:00Z",
    "updated_at": "2026-09-22T11:00:00Z",
    "scope": "ORGANIZATION",
    "editable": true,
    "versions": 1,
    "active_version": null
  },
  "version": {
    "id": "prv_01j9rz2e4g6j8m0p2r4t6w8y0a",
    "prompt_id": "prm_01j9ry9d1f3h5k7m9p1s3v5x7z",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "business_id": null,
    "agent": "SELLER",
    "version": 1,
    "content": "You are {{identity}}\n\nYou write the replies of a business in a Telegram conversation that the customer started. …\n\n# Knowledge\n{{knowledge}}",
    "status": "DRAFT",
    "changelog": "Shorter answers; always offer a person for refunds.",
    "author_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "author_label": "dilnoza@silkroad.example",
    "activated_at": null,
    "created_at": "2026-09-25T16:20:00Z",
    "updated_at": "2026-09-26T09:30:00Z"
  }
}
GET/ai/prompts/{id}/versionsai:read

A prompt's versions, newest first, with their content.

Example
Response · 200 OK
{
  "data": [
    {
      "id": "prv_01j9rz2e4g6j8m0p2r4t6w8y0a",
      "prompt_id": "prm_01j9ry9d1f3h5k7m9p1s3v5x7z",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "business_id": null,
      "agent": "SELLER",
      "version": 3,
      "content": "You are {{identity}}\n\nYou write the replies of a business in a Telegram conversation that the customer started. …\n\n# Knowledge\n{{knowledge}}",
      "status": "ACTIVE",
      "changelog": "Shorter answers; always offer a person for refunds.",
      "author_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "author_label": "dilnoza@silkroad.example",
      "activated_at": "2026-09-26T09:30:00Z",
      "created_at": "2026-09-25T16:20:00Z",
      "updated_at": "2026-09-26T09:30:00Z"
    },
    {
      "id": "prv_01j9s6dm5p7r9t1v3x5z7b9d1f",
      "prompt_id": "prm_01j9ry9d1f3h5k7m9p1s3v5x7z",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "business_id": null,
      "agent": "SELLER",
      "version": 2,
      "content": "You are {{identity}}\n\nYou write the replies of a business in a Telegram conversation that the customer started. …\n\n# Knowledge\n{{knowledge}}",
      "status": "ARCHIVED",
      "changelog": "Shorter answers; always offer a person for refunds.",
      "author_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "author_label": "dilnoza@silkroad.example",
      "activated_at": "2026-09-26T09:30:00Z",
      "created_at": "2026-09-25T16:20:00Z",
      "updated_at": "2026-09-26T09:30:00Z"
    }
  ],
  "total": 2,
  "limit": 50,
  "offset": 0
}
POST/ai/prompts/{id}/versionsai:manage

Add a version to one of your prompts (platform prompts can't be changed: 403 platform_prompt). status is DRAFT or TESTING; activate: true makes it active at once.

Example
Request body
{
  "content": "You are {{identity}} …",
  "changelog": "Shorter answers; always offer a person for refunds.",
  "status": "TESTING"
}
Response · 201 Created
{
  "id": "prv_01j9rz2e4g6j8m0p2r4t6w8y0a",
  "prompt_id": "prm_01j9ry9d1f3h5k7m9p1s3v5x7z",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "business_id": null,
  "agent": "SELLER",
  "version": 4,
  "content": "You are {{identity}}\n\nYou write the replies of a business in a Telegram conversation that the customer started. …\n\n# Knowledge\n{{knowledge}}",
  "status": "TESTING",
  "changelog": "Shorter answers; always offer a person for refunds.",
  "author_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "author_label": "dilnoza@silkroad.example",
  "activated_at": null,
  "created_at": "2026-09-25T16:20:00Z",
  "updated_at": "2026-09-26T09:30:00Z"
}
POST/ai/prompts/{id}/versions/{v}/activateai:manage

Make version v the active version for its agent and scope. The version it replaces is archived in the same step: there is never more than one active version per organization, agent and business. Activating an archived version rolls back to it.

Example
Response · 200 OK
{
  "version": {
    "id": "prv_01j9rz2e4g6j8m0p2r4t6w8y0a",
    "prompt_id": "prm_01j9ry9d1f3h5k7m9p1s3v5x7z",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "business_id": null,
    "agent": "SELLER",
    "version": 3,
    "content": "You are {{identity}}\n\nYou write the replies of a business in a Telegram conversation that the customer started. …\n\n# Knowledge\n{{knowledge}}",
    "status": "ACTIVE",
    "changelog": "Shorter answers; always offer a person for refunds.",
    "author_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "author_label": "dilnoza@silkroad.example",
    "activated_at": "2026-09-26T09:30:00Z",
    "created_at": "2026-09-25T16:20:00Z",
    "updated_at": "2026-09-26T09:30:00Z"
  },
  "previous": {
    "id": "prv_01j9s6dm5p7r9t1v3x5z7b9d1f",
    "prompt_id": "prm_01j9ry9d1f3h5k7m9p1s3v5x7z",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "business_id": null,
    "agent": "SELLER",
    "version": 2,
    "content": "You are {{identity}}\n\nYou write the replies of a business in a Telegram conversation that the customer started. …\n\n# Knowledge\n{{knowledge}}",
    "status": "ARCHIVED",
    "changelog": "Shorter answers; always offer a person for refunds.",
    "author_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "author_label": "dilnoza@silkroad.example",
    "activated_at": "2026-09-26T09:30:00Z",
    "created_at": "2026-09-25T16:20:00Z",
    "updated_at": "2026-09-26T09:30:00Z"
  }
}
GET/ai/executionsai:read

AI runs, newest first: the agent, mode (LIVE, PLAYGROUND or EVAL), status (RUNNING, SUCCEEDED, FAILED or BLOCKED by the platform's switches or a budget), the provider and model of the main model call, tokens, cost in micro-dollars (unpriced when a price was missing), latency and the final action. conversation filters by conversation ID; from and to by start time.

Query: agent, status, mode, conversation, business_id, from, to, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "agent": "SELLER",
      "mode": "LIVE",
      "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
      "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "provider_id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
      "provider_kind": "OPENAI",
      "model": "gpt-5.6",
      "prompt_version_id": "prv_01j9rz2e4g6j8m0p2r4t6w8y0a",
      "identity_id": null,
      "communication_profile_id": null,
      "language_profile_id": null,
      "knowledge_version": null,
      "memory_refs": [],
      "jev_decision_id": null,
      "tools": ["knowledge.search"],
      "input_tokens": 2140,
      "output_tokens": 96,
      "embed_tokens": 12,
      "tokens_estimated": false,
      "cost_micros": 3636,
      "unpriced": false,
      "llm_calls": 2,
      "fallbacks": 0,
      "latency_ms": 2410,
      "status": "SUCCEEDED",
      "validation": {
        "passed": true,
        "checks": ["schema", "honesty", "grounding", "length"]
      },
      "final_action": "SEND",
      "error": null,
      "started_at": "2026-09-26T09:29:57Z",
      "finished_at": "2026-09-26T09:29:59Z",
      "created_at": "2026-09-26T09:29:57Z",
      "updated_at": "2026-09-26T09:29:59Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/ai/executions/{id}ai:read

One run with its trace: the steps in order (input summary, output, the model call each made with its tokens and cost, errors) and the tool calls the model asked for (REQUESTED, then EXECUTED, REJECTED or FAILED). Step payloads are stored encrypted. No model reasoning is stored.

Example
Response · 200 OK
{
  "id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "agent": "SELLER",
  "mode": "LIVE",
  "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
  "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "provider_id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
  "provider_kind": "OPENAI",
  "model": "gpt-5.6",
  "prompt_version_id": "prv_01j9rz2e4g6j8m0p2r4t6w8y0a",
  "identity_id": null,
  "communication_profile_id": null,
  "language_profile_id": null,
  "knowledge_version": null,
  "memory_refs": [],
  "jev_decision_id": null,
  "tools": ["knowledge.search"],
  "input_tokens": 2140,
  "output_tokens": 96,
  "embed_tokens": 12,
  "tokens_estimated": false,
  "cost_micros": 3636,
  "unpriced": false,
  "llm_calls": 2,
  "fallbacks": 0,
  "latency_ms": 2410,
  "status": "SUCCEEDED",
  "validation": {
    "passed": true,
    "checks": ["schema", "honesty", "grounding", "length"]
  },
  "final_action": "SEND",
  "error": null,
  "started_at": "2026-09-26T09:29:57Z",
  "finished_at": "2026-09-26T09:29:59Z",
  "created_at": "2026-09-26T09:29:57Z",
  "updated_at": "2026-09-26T09:29:59Z",
  "steps": [
    {
      "id": "axs_01j9rw3b5d7f9h1k3m5p7r9t1v",
      "execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
      "position": 0,
      "step": "knowledge",
      "status": "SUCCEEDED",
      "input_summary": { "query_terms": 4 },
      "output": { "chunks": 3 },
      "error": null,
      "operation": "EMBED",
      "provider_id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
      "provider_kind": "OPENAI",
      "model": "text-embedding-3-small",
      "input_tokens": 0,
      "output_tokens": 0,
      "embed_tokens": 12,
      "tokens_estimated": false,
      "cost_micros": 0,
      "unpriced": false,
      "fallbacks": 0,
      "call_failed": false,
      "started_at": "2026-09-26T09:29:57Z",
      "finished_at": "2026-09-26T09:29:57Z",
      "latency_ms": 180
    },
    {
      "id": "axs_01j9s7en8r0t2w4y6a8c0e2g4j",
      "execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
      "position": 1,
      "step": "generate",
      "status": "SUCCEEDED",
      "input_summary": { "messages": 8, "knowledge_chunks": 3 },
      "output": {
        "message": "Salom! Ha, ertaga soat 10:00 da ochiq. Sizni qaysi kun qiziqtiradi?",
        "actions": []
      },
      "error": null,
      "operation": "STRUCTURED",
      "provider_id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
      "provider_kind": "OPENAI",
      "model": "gpt-5.6",
      "input_tokens": 2140,
      "output_tokens": 96,
      "embed_tokens": 0,
      "tokens_estimated": false,
      "cost_micros": 3636,
      "unpriced": false,
      "fallbacks": 0,
      "call_failed": false,
      "started_at": "2026-09-26T09:29:57Z",
      "finished_at": "2026-09-26T09:29:59Z",
      "latency_ms": 2210
    }
  ],
  "tool_calls": [
    {
      "id": "atc_01j9rx6c8e0g2j4m6p8r0t2w4y",
      "execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
      "step_id": "axs_01j9rw3b5d7f9h1k3m5p7r9t1v",
      "tool": "knowledge.search",
      "args": { "query": "opening hours" },
      "result": { "chunks": 3 },
      "status": "EXECUTED",
      "error": null,
      "created_at": "2026-09-26T09:29:57Z",
      "finished_at": "2026-09-26T09:29:57Z",
      "latency_ms": 40
    }
  ]
}
GET/ai/costsai:read

AI usage and cost for period today, week (the last 7 UTC days) or month (the calendar month so far, the default), in total and by business, agent, provider, model and mode, plus a day-by-day series. Requests and tokens are what providers reported (observed); cost is derived from the registry's prices at the time of each call.

Query: period

Example
Response · 200 OK
{
  "period": "month",
  "from": "2026-09-01",
  "to": "2026-09-26",
  "currency": "USD",
  "basis": { "requests": "observed", "tokens": "observed", "cost": "derived" },
  "totals": {
    "requests": 1840,
    "failed_requests": 1,
    "input_tokens": 3496000,
    "output_tokens": 202400,
    "embed_tokens": 25760,
    "cost_micros": 6412300,
    "cost_usd": 6.4123,
    "unpriced_requests": 0,
    "estimated_requests": 0
  },
  "by_business": [
    {
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "requests": 1790,
      "failed_requests": 1,
      "input_tokens": 3401000,
      "output_tokens": 196900,
      "embed_tokens": 25060,
      "cost_micros": 6231000,
      "cost_usd": 6.231,
      "unpriced_requests": 0,
      "estimated_requests": 0
    }
  ],
  "by_agent": [
    {
      "agent": "SELLER",
      "requests": 1210,
      "failed_requests": 1,
      "input_tokens": 2299000,
      "output_tokens": 133100,
      "embed_tokens": 16940,
      "cost_micros": 5102000,
      "cost_usd": 5.102,
      "unpriced_requests": 0,
      "estimated_requests": 0
    }
  ],
  "by_provider": [
    {
      "provider_id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
      "provider_kind": "OPENAI",
      "provider_name": "OpenAI",
      "requests": 1840,
      "failed_requests": 1,
      "input_tokens": 3496000,
      "output_tokens": 202400,
      "embed_tokens": 25760,
      "cost_micros": 6412300,
      "cost_usd": 6.4123,
      "unpriced_requests": 0,
      "estimated_requests": 0
    }
  ],
  "by_model": [
    {
      "provider_kind": "OPENAI",
      "model": "gpt-5.6",
      "requests": 1210,
      "failed_requests": 1,
      "input_tokens": 2299000,
      "output_tokens": 133100,
      "embed_tokens": 16940,
      "cost_micros": 5102000,
      "cost_usd": 5.102,
      "unpriced_requests": 0,
      "estimated_requests": 0
    }
  ],
  "by_mode": [
    {
      "mode": "LIVE",
      "requests": 1760,
      "failed_requests": 1,
      "input_tokens": 3344000,
      "output_tokens": 193600,
      "embed_tokens": 24640,
      "cost_micros": 6201000,
      "cost_usd": 6.201,
      "unpriced_requests": 0,
      "estimated_requests": 0
    }
  ],
  "daily": [
    {
      "date": "2026-09-26",
      "requests": 84,
      "failed_requests": 1,
      "input_tokens": 159600,
      "output_tokens": 9240,
      "embed_tokens": 1176,
      "cost_micros": 301400,
      "cost_usd": 0.3014,
      "unpriced_requests": 0,
      "estimated_requests": 0
    }
  ]
}
GET/ai/budgetsai:read

The organization's AI budgets (daily and monthly, each null when unset) with this period's spending, and the combined state: ALLOWED, SOFT_ALERT (an alert threshold reached, or the limit passed on a budget without hard stop) or HARD_STOP (runs and model calls are refused until the period resets).

Example
Response · 200 OK
{
  "state": "SOFT_ALERT",
  "daily": null,
  "monthly": {
    "id": "abg_01j9s15f7h9k1m3p5r7t9v1x3z",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "period": "MONTHLY",
    "limit_micros": 50000000,
    "alert_thresholds": [80, 100],
    "hard_stop": true,
    "updated_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "created_at": "2026-09-20T08:00:00Z",
    "updated_at": "2026-09-20T08:00:00Z",
    "limit_usd": 50,
    "spent_micros": 41200000,
    "spent_usd": 41.2,
    "ratio": 0.824,
    "state": "SOFT_ALERT",
    "period_start": "2026-09-01T00:00:00Z",
    "resets_at": "2026-10-01T00:00:00Z"
  }
}
PUT/ai/budgetsai:manage

Set the budgets: daily and monthly each take {limit_usd, alert_thresholds, hard_stop}, null removes that budget, and a period you leave out stays as it is. alert_thresholds are percentages of the limit (1 to 1000, default 80 and 100): crossing one notifies the organization once per period. hard_stop (default true) stops AI at 100% until the period resets. Changing the limit or thresholds re-arms this period's alerts.

Example
Request body
{
  "monthly": {
    "limit_usd": 50,
    "alert_thresholds": [80, 100],
    "hard_stop": true
  }
}
Response · 200 OK
{
  "state": "SOFT_ALERT",
  "daily": null,
  "monthly": {
    "id": "abg_01j9s15f7h9k1m3p5r7t9v1x3z",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "period": "MONTHLY",
    "limit_micros": 50000000,
    "alert_thresholds": [80, 100],
    "hard_stop": true,
    "updated_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "created_at": "2026-09-20T08:00:00Z",
    "updated_at": "2026-09-20T08:00:00Z",
    "limit_usd": 50,
    "spent_micros": 41200000,
    "spent_usd": 41.2,
    "ratio": 0.824,
    "state": "SOFT_ALERT",
    "period_start": "2026-09-01T00:00:00Z",
    "resets_at": "2026-10-01T00:00:00Z"
  }
}

AI Seller: conversations#

The private Telegram chats of accounts with inbound turned on (PATCH /accounts/{id}, inbound_enabled), and the Instagram Direct and Facebook Messenger threads of connected accounts (see Channels: Instagram and Facebook), in one inbox: channel says which. A worker keeps one connection per such ACTIVE account and records every chat with a person, in both directions: author CUSTOMER, AI, HUMAN (from the console or the API) or OWNER_PHONE (typed in the account's own Telegram app). Groups, channels and bots are left out. Message text is encrypted at rest. People who write in become recipients with consent UNKNOWN (source INBOUND) until the plan's recipient limit. Oqim replies only inside a conversation the customer started, within the reply window (24 hours by default); outside it, only on recorded consent. A stop request ("stop", "стоп", "toʻxtating", "kerak emas", …) opts the person out and suppresses them for good. Live changes arrive on the event stream as ai.conversation.* and ai.handoff.* events. When an account serves a business whose Seller is active (seller-settings active), the AI takes a turn a few seconds after the customer writes: it reads the conversation, what it remembers and the business's knowledge, judges the message (JEV), picks the funnel stage, writes a reply and validates it (honesty, disclosure, facts only from the offers and knowledge, language, length, stage rules, no requests for secrets), then the business's autonomy level decides: 0 analytics only, 1 a recommendation, 2 a draft a person approves, 3 automatic only for the approved scenarios, 4 automatic, 5 automatic with approved tools. A customer who asks for a person or complains is handed to a person (a handoff) instead, and so is one who wants a booking unless the organization connected a Google calendar (then the Calendar agent handles it before the reply: see AI Seller: calendar); one who asks to stop is opted out. Every turn is an execution (GET /ai/executions/{id}) and emits ai.turn.completed; replies emit ai.reply.drafted, ai.reply.blocked and, when delivered, ai.reply.sent; stage moves ai.stage.changed; leads ai.lead.updated; drafts ai.draft.updated. Personal chats (personal true) are never the AI's: the account's Telegram contacts become personal by themselves (personal_source CONTACT) and an owner marks any chat personal or not (OWNER, never overridden automatically). The AI never answers or analyses a personal chat, what it had worked out about one is removed, and the list hides them unless asked; people still read and answer them. Messages imported from a chat's past (POST /accounts/{id}/history/import on Telegram, POST /channels/accounts/{id}/history/import on Instagram and Facebook) have imported true and keep the platform's time: they never start a turn, a draft or an analysis, open no reply window, don't count as unread and fire no event; the AI reads only live messages.

GET/ai/conversationsai:read

The organization's inbox on every channel, each with its channel (TELEGRAM, INSTAGRAM or FACEBOOK), its account (account: id, channel, name, username, avatar_url; connected_account_id for Instagram and Facebook), the linked recipient's consent, last_message (author, kind, the first 80 characters of the text decrypted, at) and waiting_since (the customer's last message time when they wrote last and nobody replied; null otherwise). Sort with sort=recent (default: last message), unread (unread first), waiting (customer waiting, oldest first) or oldest. Filter by channel, account_id (a Telegram account or a connected account), ai_state (ACTIVE, PAUSED_HANDOFF, STOPPED or OFF), unread (true or false), waiting (true or false), has_media (true or false), personal (true lists only personal chats and needs accounts:manage; false, the default, hides them) and q (name, @username, phone, Telegram ID or Instagram/Page-scoped ID).

Query: channel, account_id, ai_state, unread, waiting, has_media, personal, sort, q, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "channel": "TELEGRAM",
      "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "connected_account_id": null,
      "peer_user_id": 6021437788,
      "peer_external_id": null,
      "recipient_id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
      "title": "Aziza Karimova",
      "username": "aziza_k",
      "language": "uz",
      "ai_state": "ACTIVE",
      "ai_state_reason": null,
      "ai_state_changed_at": "2026-09-26T09:12:00Z",
      "last_message_at": "2026-09-26T09:14:05Z",
      "last_inbound_at": "2026-09-26T09:14:05Z",
      "last_analyzed_message_id": null,
      "unread_count": 2,
      "opted_out_at": null,
      "personal": false,
      "personal_source": null,
      "history_imported_at": null,
      "created_at": "2026-09-26T09:12:00Z",
      "updated_at": "2026-09-26T09:14:05Z",
      "account_name": "Silk Road Support",
      "account": {
        "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
        "channel": "TELEGRAM",
        "name": "Silk Road Support",
        "username": "silkroad_support",
        "avatar_url": null
      },
      "consent_status": "OPTED_IN",
      "last_message": {
        "id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
        "direction": "IN",
        "author": "CUSTOMER",
        "status": "RECEIVED",
        "text": "Salom! Expo chiptasi qancha turadi?",
        "deleted": false,
        "occurred_at": "2026-09-26T09:14:05Z"
      }
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/ai/conversations/{id}ai:read

One conversation with its newest messages (oldest first; limit up to 200, default 50), the linked recipient, the open handoff, and reply: whether a person can reply now and on what basis (REPLY_WINDOW, CONSENT on Telegram, or HUMAN_AGENT on Instagram and Facebook, with window_ends_at and, where the platform's Meta app has the Human Agent permission, human_window_ends_at), or why not (code, reason), and max_length with max_length_unit: the channel's text limit (Telegram 4096 utf16, Instagram 1000 bytes, Facebook 2000 characters). On Instagram and Facebook the window is 24 hours after the customer's last message (the business's reply window can shorten it); after that only a person may answer, up to 7 days, with Meta's HUMAN_AGENT tag, and nothing goes out on consent (code window_closed). Messages carry external_message_id (Meta's mid), imported (from a history import), read_at and reaction where Meta reports them, reply_to_message_id, forwarded_from, reactions ([{emoji, by: CUSTOMER|BUSINESS}]), attachments (id, kind, status, mime, filename, size_bytes, width, height, duration_ms, has_thumb; served by GET …/attachments/{attachment_id}), and metadata describing locations and contacts. Pass next_cursor as before for older messages. A message the customer deleted keeps its metadata but not its text. The AI Seller's side: state (what it knows about the customer: stage, intent with its confidence, interest, lead quality, objections, language, sentiment, next action, and details — needs, pain points, budget, timeline, whether they decide alone — which are encrypted at rest; null before the Seller's first turn), draft (the draft or recommendation waiting for a person, with its text, or null), lead, and stage_events (the last 20 moves between funnel stages, oldest first).

Query: limit, before

Example
Response · 200 OK
{
  "conversation": {
    "id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "channel": "TELEGRAM",
    "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
    "connected_account_id": null,
    "peer_user_id": 6021437788,
    "peer_external_id": null,
    "recipient_id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
    "title": "Aziza Karimova",
    "username": "aziza_k",
    "language": "uz",
    "ai_state": "ACTIVE",
    "ai_state_reason": null,
    "ai_state_changed_at": "2026-09-26T09:12:00Z",
    "last_message_at": "2026-09-26T09:14:05Z",
    "last_inbound_at": "2026-09-26T09:14:05Z",
    "last_analyzed_message_id": null,
    "unread_count": 2,
    "opted_out_at": null,
    "personal": false,
    "personal_source": null,
    "history_imported_at": null,
    "created_at": "2026-09-26T09:12:00Z",
    "updated_at": "2026-09-26T09:14:05Z",
    "account_name": "Silk Road Support",
    "account": {
      "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "channel": "TELEGRAM",
      "name": "Silk Road Support",
      "username": "silkroad_support",
      "avatar_url": null
    },
    "consent_status": "OPTED_IN",
    "last_message": {
      "id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
      "direction": "IN",
      "author": "CUSTOMER",
      "status": "RECEIVED",
      "text": "Salom! Expo chiptasi qancha turadi?",
      "deleted": false,
      "occurred_at": "2026-09-26T09:14:05Z"
    }
  },
  "recipient": {
    "id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "telegram_id": 123456789,
    "username": "aziza_k",
    "first_name": "Aziza",
    "last_name": "Karimova",
    "language": "uz",
    "source": "CSV",
    "consent_status": "OPTED_IN",
    "consent_timestamp": "2026-08-14T11:20:00Z",
    "consent_note": "Registered for Tashkent Expo 2026 on expo.example",
    "opted_out_at": null,
    "opt_out_source": null,
    "attributes": { "event_date": "1 October", "location": "Tashkent City Hall" },
    "tags": ["expo-2026", "vip"],
    "imported_at": "2026-09-05T09:00:00Z",
    "created_at": "2026-09-05T09:00:00Z",
    "updated_at": "2026-09-05T09:00:00Z",
    "opt_out_status": false,
    "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"]
  },
  "handoff": null,
  "reply": {
    "allowed": true,
    "basis": "REPLY_WINDOW",
    "window_ends_at": "2026-09-27T09:14:05Z",
    "max_length": 4096,
    "max_length_unit": "utf16"
  },
  "messages": [
    {
      "id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
      "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "telegram_message_id": 48213,
      "external_message_id": null,
      "direction": "IN",
      "author": "CUSTOMER",
      "body_len": 35,
      "media_kind": null,
      "execution_id": null,
      "sent_by": null,
      "status": "RECEIVED",
      "send_basis": null,
      "attempts": 0,
      "error_code": null,
      "error": null,
      "occurred_at": "2026-09-26T09:14:05Z",
      "sent_at": "2026-09-26T09:14:05Z",
      "edited_at": null,
      "deleted_at": null,
      "imported": false,
      "read_at": null,
      "reaction": null,
      "metadata": null,
      "created_at": "2026-09-26T09:14:06Z",
      "updated_at": "2026-09-26T09:14:06Z",
      "body": "Salom! Expo chiptasi qancha turadi?"
    }
  ],
  "next_cursor": null,
  "state": {
    "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
    "stage": "OFFER_PRESENTED",
    "sales_stage_id": "stg_01j9s2c4e6g8j0m2p4r6t8w0y2",
    "intent": "pricing_question",
    "intent_confidence": 0.9,
    "interest_level": "high",
    "interest_score": 3.8,
    "lead_quality": "warm",
    "objection": "none",
    "objections": [],
    "offers_interested": ["ofr_01j9s4m7p9r1t3v5x7z9b1d3f5"],
    "language": "uz",
    "sentiment": "positive",
    "next_action": "ANSWER_QUESTION",
    "human_handoff": false,
    "last_message_id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
    "last_message_at": "2026-09-26T09:14:05Z",
    "last_execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
    "turns": 1,
    "consecutive_failures": 0,
    "fired_triggers": [],
    "created_at": "2026-09-26T09:14:08Z",
    "updated_at": "2026-09-26T09:14:11Z",
    "details": {
      "needs": ["a one-day ticket"],
      "pain_points": [],
      "budget": null,
      "timeline": "this Saturday",
      "decision_maker": true
    }
  },
  "draft": {
    "id": "sdr_01j9s4h4k6n8q0s2u4w6y8a0c2",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
    "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
    "execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
    "kind": "DRAFT",
    "status": "PENDING",
    "body_len": 92,
    "language": "uz",
    "actions": ["ANSWER_QUESTION"],
    "validation": {
      "attempts": 1,
      "passed": true,
      "disclosure": "added",
      "failures": [],
      "warnings": [],
      "checks": [
        { "name": "honesty", "passed": true },
        { "name": "grounding", "passed": true },
        { "name": "language", "passed": true }
      ]
    },
    "reply_to_message_id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
    "message_id": null,
    "decided_by": null,
    "decided_at": null,
    "created_at": "2026-09-26T09:14:11Z",
    "updated_at": "2026-09-26T09:14:11Z",
    "body": "Bu xabarga AI yordamchi javob bermoqda.\n\nSalom! Chipta narxi 150 000 soʻm. Qaysi kunga kerak?"
  },
  "lead": {
    "id": "led_01j9s4j5m7p9r1t3v5x7z9b1d3",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
    "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
    "recipient_id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
    "title": "Aziza Karimova",
    "stage": "OFFER_PRESENTED",
    "quality": "warm",
    "interest": "high",
    "status": "OPEN",
    "value_estimate": 150000,
    "currency": "UZS",
    "offer_ids": ["ofr_01j9s4m7p9r1t3v5x7z9b1d3f5"],
    "owner_user_id": null,
    "last_execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
    "closed_at": null,
    "created_at": "2026-09-26T09:14:11Z",
    "updated_at": "2026-09-26T09:14:11Z",
    "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
    "stage_name": "Offer Presented"
  },
  "stage_events": [
    {
      "id": "sse_01j9s4k5m7p9r1t3v5x7z9b1d3",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
      "from": null,
      "to": "NEW",
      "source": "SYSTEM",
      "reason": "The conversation starts at the funnel's first stage.",
      "confidence": null,
      "execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
      "jev_decision_id": null,
      "created_at": "2026-09-26T09:14:11Z"
    },
    {
      "id": "sse_01j9s4k6n8q0s2u4w6y8a0c2e4",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
      "from": "NEW",
      "to": "OFFER_PRESENTED",
      "source": "AI",
      "reason": "The decision layer places the conversation in OFFER_PRESENTED (confidence 0.90).",
      "confidence": 0.9,
      "execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
      "jev_decision_id": "jvd_01j9rs4w6y8a0c2e4g6j8m0p2r",
      "created_at": "2026-09-26T09:14:11Z"
    }
  ]
}
POST/ai/conversations/{id}/replyai:operate

Reply as a person. The text (plain, up to the channel's max_length: 4,096 characters on Telegram, 1,000 bytes on Instagram, 2,000 characters on Facebook) is queued and sent by a worker from the conversation's account, like the AI's replies: at least a few seconds after the previous reply in the conversation, within the account's cooldowns and daily limit, and never from another account; Instagram and Facebook replies go through Meta's Send API as plain text. The consent gate is checked now and again at sending: 409 reply_not_allowed with details.reason OPTED_OUT, SUPPRESSED, CONVERSATION_STOPPED, NO_CONSENT (the reply window closed and there's no recorded consent) or window_closed (Instagram and Facebook: past 24 hours, or past 7 days with the Human Agent permission); 409 account_not_active, account_auth_required (reconnect the Instagram account or Page) or organization_suspended. A send Meta refuses ends FAILED with error_code window_closed, account_auth_required (the account becomes AUTH_REQUIRED), recipient_unavailable or provider_error; rate limits are retried. Watch ai.conversation.message_updated for sent or failed. Needs an accepted acceptable-use policy.

Example
Request body
{
  "text": "Salom Aziza! Chiptalar 150 000 so'mdan. Havolani yuboraymi?"
}
Response · 202 Accepted
{
  "id": "msg_01j9s4f2h4k6n8q0s2u4w6y8a0",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
  "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "telegram_message_id": null,
  "external_message_id": null,
  "direction": "OUT",
  "author": "HUMAN",
  "body_len": 58,
  "media_kind": null,
  "execution_id": null,
  "sent_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "status": "QUEUED",
  "send_basis": "REPLY_WINDOW",
  "attempts": 0,
  "error_code": null,
  "error": null,
  "occurred_at": "2026-09-26T09:30:00Z",
  "sent_at": null,
  "edited_at": null,
  "deleted_at": null,
  "imported": false,
  "read_at": null,
  "reaction": null,
  "metadata": null,
  "created_at": "2026-09-26T09:30:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "body": "Salom Aziza! Chiptalar 150 000 so'mdan. Havolani yuboraymi?"
}
POST/ai/conversations/{id}/takeoverai:operate

Take the conversation over: the AI pauses (ai_state PAUSED_HANDOFF), its unsent messages are cancelled, and a MANUAL handoff assigned to you is opened (or the open handoff, if unassigned, becomes yours). A conversation the customer stopped is 409 conversation_stopped.

Example
Response · 200 OK
{
  "conversation": {
    "id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "channel": "TELEGRAM",
    "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
    "connected_account_id": null,
    "peer_user_id": 6021437788,
    "peer_external_id": null,
    "recipient_id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
    "title": "Aziza Karimova",
    "username": "aziza_k",
    "language": "uz",
    "ai_state": "PAUSED_HANDOFF",
    "ai_state_reason": "Taken over by dilnoza@silkroad.example",
    "ai_state_changed_at": "2026-09-26T09:12:00Z",
    "last_message_at": "2026-09-26T09:14:05Z",
    "last_inbound_at": "2026-09-26T09:14:05Z",
    "last_analyzed_message_id": null,
    "unread_count": 0,
    "opted_out_at": null,
    "personal": false,
    "personal_source": null,
    "history_imported_at": null,
    "created_at": "2026-09-26T09:12:00Z",
    "updated_at": "2026-09-26T09:14:05Z",
    "account_name": "Silk Road Support",
    "account": {
      "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "channel": "TELEGRAM",
      "name": "Silk Road Support",
      "username": "silkroad_support",
      "avatar_url": null
    },
    "consent_status": "OPTED_IN",
    "last_message": {
      "id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
      "direction": "IN",
      "author": "CUSTOMER",
      "status": "RECEIVED",
      "text": "Salom! Expo chiptasi qancha turadi?",
      "deleted": false,
      "occurred_at": "2026-09-26T09:14:05Z"
    }
  },
  "handoff": {
    "id": "hof_01j9s4g3j5m7p9r1t3v5x7z9b1",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
    "reason": "Taken over by dilnoza@silkroad.example",
    "trigger": "MANUAL",
    "ai_state": {},
    "recommended_action": null,
    "execution_id": null,
    "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "assigned_user": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "resolved_at": null,
    "resolved_by": null,
    "resolution_note": null,
    "created_at": "2026-09-26T09:20:00Z",
    "updated_at": "2026-09-26T09:20:00Z",
    "conversation_title": "Aziza Karimova",
    "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9"
  }
}
POST/ai/conversations/{id}/resumeai:operate

Hand the conversation back to the AI: ai_state ACTIVE and the open handoff resolved. The AI then answers whatever the customer wrote meanwhile. A conversation the customer stopped can't be resumed (409 conversation_stopped).

Example
Response · 200 OK
{
  "conversation": {
    "id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "channel": "TELEGRAM",
    "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
    "connected_account_id": null,
    "peer_user_id": 6021437788,
    "peer_external_id": null,
    "recipient_id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
    "title": "Aziza Karimova",
    "username": "aziza_k",
    "language": "uz",
    "ai_state": "ACTIVE",
    "ai_state_reason": null,
    "ai_state_changed_at": "2026-09-26T09:12:00Z",
    "last_message_at": "2026-09-26T09:14:05Z",
    "last_inbound_at": "2026-09-26T09:14:05Z",
    "last_analyzed_message_id": null,
    "unread_count": 2,
    "opted_out_at": null,
    "personal": false,
    "personal_source": null,
    "history_imported_at": null,
    "created_at": "2026-09-26T09:12:00Z",
    "updated_at": "2026-09-26T09:14:05Z",
    "account_name": "Silk Road Support",
    "account": {
      "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "channel": "TELEGRAM",
      "name": "Silk Road Support",
      "username": "silkroad_support",
      "avatar_url": null
    },
    "consent_status": "OPTED_IN",
    "last_message": {
      "id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
      "direction": "IN",
      "author": "CUSTOMER",
      "status": "RECEIVED",
      "text": "Salom! Expo chiptasi qancha turadi?",
      "deleted": false,
      "occurred_at": "2026-09-26T09:14:05Z"
    }
  },
  "handoff": null
}
POST/ai/conversations/{id}/readai:operate

Mark the conversation read in Oqim only (unread_count 0). Nothing is marked read on Telegram, Instagram or Facebook: the customer never sees a read receipt. Replying or taking over does this too.

Example
Response · 200 OK
{
  "id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "channel": "TELEGRAM",
  "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "connected_account_id": null,
  "peer_user_id": 6021437788,
  "peer_external_id": null,
  "recipient_id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
  "title": "Aziza Karimova",
  "username": "aziza_k",
  "language": "uz",
  "ai_state": "ACTIVE",
  "ai_state_reason": null,
  "ai_state_changed_at": "2026-09-26T09:12:00Z",
  "last_message_at": "2026-09-26T09:14:05Z",
  "last_inbound_at": "2026-09-26T09:14:05Z",
  "last_analyzed_message_id": null,
  "unread_count": 0,
  "opted_out_at": null,
  "personal": false,
  "personal_source": null,
  "history_imported_at": null,
  "created_at": "2026-09-26T09:12:00Z",
  "updated_at": "2026-09-26T09:14:05Z",
  "account_name": "Silk Road Support",
  "account": {
    "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
    "channel": "TELEGRAM",
    "name": "Silk Road Support",
    "username": "silkroad_support",
    "avatar_url": null
  },
  "consent_status": "OPTED_IN",
  "last_message": {
    "id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
    "direction": "IN",
    "author": "CUSTOMER",
    "status": "RECEIVED",
    "text": "Salom! Expo chiptasi qancha turadi?",
    "deleted": false,
    "occurred_at": "2026-09-26T09:14:05Z"
  }
}
POST/ai/conversations/{id}/platform-readai:operate

Send a read receipt on the platform itself (Telegram readHistory, Meta mark_seen) — the ONLY way a message is ever marked read there, and only when a person asks. Sends it exactly once: a second call answers 200 with already_read: true and sends nothing. 409 when the platform refuses, 503 when the account is unreachable (nothing was sent). Audited.

Example
Response · 200 OK
{
  "platform_read": true,
  "already_read": false
}
GET/ai/conversations/{id}/attachments/{attachment_id}ai:read

Stream a message's file from Oqim's storage with its content type and HTTP Range (audio and video seek). The platform's URL or file reference is never exposed. ?thumb=1 returns the thumbnail where one exists (404 no_thumbnail otherwise). While the file is still downloading it answers 202 { status: "PENDING" } and queues the download; a file that is TOO_LARGE, UNAVAILABLE or FAILED answers 409 with status and error. In a personal chat the route needs accounts:manage (otherwise 404).

Query: thumb

Events: ai.conversation.attachment_ready

Example
Response · 200 OK
{
  "status": "PENDING"
}
POST/ai/conversations/{id}/uploadsai:operate

Upload one file (multipart field file) to send in the conversation, before replying. Returns upload_id, filename, mime, size_bytes, kind and expires_at (24 h). It expires unused. Kinds allowed by channel: Telegram any file up to the cap (INBOX_MAX_FILE_MB, default 50 MB), Instagram image/video/audio, Facebook image/video/audio/file; 422 kind_not_allowed otherwise, 413 file_too_large. Send it with POST …/reply (attachments: [upload_id]).

Example
Request body
{
  "file": "(multipart file)"
}
Response · 201 Created
{
  "upload_id": "cup_01j9t5x7z9b1d3f5h7k9m1p3r5",
  "filename": "price-list.pdf",
  "mime": "application/pdf",
  "size_bytes": 184320,
  "kind": "DOCUMENT",
  "expires_at": "2026-09-26T09:30:00Z"
}
PUT/ai/conversations/{id}/personalai:operate

Mark the chat personal (personal: true) or a business chat (false). The decision is yours (personal_source OWNER): contacts never override it, and history imports respect it. A personal chat is never answered or analysed by the AI: its pending draft is superseded, the AI's unsent messages are cancelled (ai.draft.updated, ai.conversation.message_updated), and its summary, insights, memories, the Seller's state and case analyses are removed; its messages stay and people can still reply. Made a business chat again, the AI answers from the customer's next message, never what was said while it was personal. Audited.

Example
Request body
{
  "personal": true
}
Response · 200 OK
{
  "id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "channel": "TELEGRAM",
  "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "connected_account_id": null,
  "peer_user_id": 6021437788,
  "peer_external_id": null,
  "recipient_id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
  "title": "Aziza Karimova",
  "username": "aziza_k",
  "language": "uz",
  "ai_state": "ACTIVE",
  "ai_state_reason": null,
  "ai_state_changed_at": "2026-09-26T09:12:00Z",
  "last_message_at": "2026-09-26T09:14:05Z",
  "last_inbound_at": "2026-09-26T09:14:05Z",
  "last_analyzed_message_id": null,
  "unread_count": 2,
  "opted_out_at": null,
  "personal": true,
  "personal_source": "OWNER",
  "history_imported_at": null,
  "created_at": "2026-09-26T09:12:00Z",
  "updated_at": "2026-09-26T09:14:05Z",
  "account_name": "Silk Road Support",
  "account": {
    "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
    "channel": "TELEGRAM",
    "name": "Silk Road Support",
    "username": "silkroad_support",
    "avatar_url": null
  },
  "consent_status": "OPTED_IN",
  "last_message": {
    "id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
    "direction": "IN",
    "author": "CUSTOMER",
    "status": "RECEIVED",
    "text": "Salom! Expo chiptasi qancha turadi?",
    "deleted": false,
    "occurred_at": "2026-09-26T09:14:05Z"
  }
}
GET/ai/conversations/{id}/insightsai:read

Latest Conversation Intelligence analysis and summary for a conversation, with live: the case analysis's current reading (JEV between summaries: scores, flags and classification; see AI Seller: case analysis and insights). Your model writes the summary and insights at most every 10 new messages and when the conversation ends; in between, sentiment and interest follow the live reading. Returns 404 (no_insights) when neither has run yet.

Example
Response · 200 OK
{
  "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
  "summary_short": "Customer wants to buy two expo tickets for Saturday.",
  "execution_id": "aix_01JXYZ",
  "live": {
    "analysis_id": "can_01j9s8u4w6y8a0c2e4g6j8m0p2",
    "phase": "LIVE",
    "mode": "JEV",
    "analyzed_at": "2026-09-26T09:33:00Z",
    "scores": {
      "satisfaction": 4.1,
      "sentiment": 4.3,
      "purchase_intent": 3.2,
      "urgency": 1.4,
      "trust": 2.2
    },
    "flags": {
      "unanswered_question": 0.12,
      "missed_opportunity": 0.08,
      "competitor_mentioned": 0.02,
      "unavailable_item_requested": 0.05,
      "complaint": 0.03
    },
    "cases": [
      {
        "dimension": "DEMAND",
        "kind": "CASE",
        "case_id": "cas_01j9s7t3v5x7z9b1d3f5h7k9m1",
        "case_key": "GROUP_TICKETS",
        "name": {
          "uz": "Guruh chiptalari",
          "ru": "Групповые билеты",
          "en": "Group tickets"
        },
        "probability": 0.86,
        "confidence": 0.86,
        "source": "JEV"
      }
    ]
  },
  "insights": {
    "intent": "purchase",
    "sentiment": "positive",
    "interest": "high",
    "lead_quality": "hot",
    "objections": [],
    "questions": ["Is Saturday still available?"],
    "products": ["Expo ticket"],
    "needs": ["Two tickets for Saturday"],
    "next_action": "Confirm availability and send payment link",
    "appointment": false,
    "summary_short": "Customer wants to buy two expo tickets for Saturday.",
    "summary_long": "The customer Aziza asked about expo ticket prices. She confirmed interest in two tickets for Saturday and asked about availability. Sentiment is positive and intent is clearly purchase.",
    "opt_out_signal": false,
    "confidence": 0.92
  }
}
POST/ai/conversations/{id}/analyzeai:operate

Queue a Conversation Intelligence summary and insights for this conversation immediately (outside the normal 60-second debounce window and the every-10-messages cadence). Returns 202 when queued, or the result directly when no queue is configured. A personal chat is never analysed: 409 conversation_personal. Rate-limited.

Example
Response · 202 Accepted
{
  "queued": true,
  "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b"
}
POST/ai/conversations/simulateai:operate

Simulator only (TELEGRAM_DRIVER=simulator; otherwise 404 simulator_only): a customer writes to one of your accounts. The message goes through exactly what a real one does: stored encrypted, the recipient linked or created, stop requests applied, events emitted and the AI's turn scheduled. from.contact true simulates someone in the account's Telegram contacts: the chat becomes personal and the AI stays out (personal true, respond false). The account must be ACTIVE with inbound on (409 account_not_active, inbound_disabled).

Example
Request body
{
  "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "from": {
    "telegram_id": 6021437788,
    "username": "aziza_k",
    "first_name": "Aziza",
    "last_name": "Karimova",
    "language_code": "uz",
    "contact": false
  },
  "text": "Salom! Expo chiptasi qancha turadi?"
}
Response · 201 Created
{
  "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
  "message_id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
  "created": true,
  "duplicate": false,
  "opted_out": false,
  "respond": true,
  "personal": false
}
POST/ai/playground/runai:operate

Dry-run the AI Seller pipeline against a business with a synthetic conversation (mode PLAYGROUND, or EVAL for automated evaluations). The pipeline runs in full — JEV decision, strategy, compose, generate, validate, business and platform policy, autonomy gate — but nothing is sent, drafted, handed off, or written to customer states, leads, memories or any mutable table. Only the execution trace is stored (mode=PLAYGROUND) and is inspectable at GET /ai/executions/{id}. The budget gate still applies. Supply at least one CUSTOMER message. Overrides: autonomy_level (–1 uses the stored value), prompt_version_id (a Seller version of any status; an unknown one is 422), communication_profile_id, language_profile_id, language, knowledge_enabled (false to skip retrieval), memory_items (extra facts for this run only), and provider_id and model: the run is pinned to exactly that provider and model (the reply, its regeneration and the decision layer's LLM fallback), with no fallback to the business's chain; provider_id alone uses the provider's own model, model alone runs on the provider the Seller's chain starts with; a provider that isn't the organization's, or a model id with spaces or over 200 characters, is 422 on that field. A pinned model that fails ends the run with final_action FAILED and the reason in error. provider_id and model in the response are what wrote the reply. The run's JEV decision is stored with mode PLAYGROUND (or EVAL) and linked to the run. Rate-limited to 60 runs per org per hour.

Example
Request body
{
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "mode": "PLAYGROUND",
  "autonomy_level": -1,
  "messages": [
    {
      "author": "CUSTOMER",
      "text": "Salom! Expo chiptasi qancha turadi?",
      "at": "2026-09-26T09:30:00Z"
    },
    {
      "author": "AI",
      "text": "Salom! Chipta narxi 150 000 soʻm.",
      "at": "2026-09-26T09:30:00Z"
    },
    {
      "author": "CUSTOMER",
      "text": "Yetkazib berasizmi?",
      "at": "2026-09-26T09:30:00Z"
    }
  ],
  "customer_state": {
    "stage_key": "OFFER_PRESENTED",
    "intent": "PRICING_QUESTION",
    "interest_level": "WARM"
  },
  "knowledge_enabled": true,
  "memory_items": ["Customer prefers Uzbek"]
}
Response · 200 OK
{
  "execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
  "final_action": "SEND",
  "reply": "Chiptalar elektron, Telegram orqali yuboriladi. Yana savolingiz bormi?",
  "validation": {
    "passed": true,
    "disclosure": "not_needed",
    "gate": "SEND",
    "attempts": 1
  },
  "validation_codes": [],
  "gate": { "autonomy": 4, "action": "SEND", "scenario": null },
  "jev_decision": {
    "decision_id": "jdec_01j9s3m5p7",
    "source": "JEV",
    "intent": "PRODUCT_QUESTION",
    "stage": "OFFER_PRESENTED",
    "next_action": "ANSWER_QUESTION",
    "interest_level": "WARM",
    "lead_quality": "warm",
    "objection": "NONE",
    "human_handoff": false,
    "opt_out": false,
    "asking_if_ai": false
  },
  "stage_change": null,
  "handoff_trigger": null,
  "block_codes": [],
  "cost_micros": 3820,
  "latency_ms": 1240,
  "provider_id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
  "model": "deepseek-flash",
  "skipped": ""
}
GET/ai/handoffsai:read

Conversations handed to people, newest first: by the AI (trigger HUMAN_REQUESTED, LOW_CONFIDENCE, COMPLAINT, PRICING_EXCEPTION, …) or taken over (MANUAL). status is open (default), resolved or all; assigned_user_id is a user ID, me or none.

Query: status, assigned_user_id, conversation_id, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "hof_01j9s4g3j5m7p9r1t3v5x7z9b1",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
      "reason": "The customer asks for a discount for a group of 40.",
      "trigger": "PRICING_EXCEPTION",
      "ai_state": { "stage": "negotiation", "intent": "group_booking" },
      "recommended_action": "Offer the group rate from the price list, or call back.",
      "execution_id": null,
      "created_by": null,
      "assigned_user": null,
      "resolved_at": null,
      "resolved_by": null,
      "resolution_note": null,
      "created_at": "2026-09-26T09:20:00Z",
      "updated_at": "2026-09-26T09:20:00Z",
      "conversation_title": "Aziza Karimova",
      "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
PATCH/ai/handoffs/{id}ai:operate

Assign an open handoff to a member (assigned_user_id; null unassigns) and/or resolve it (resolved: true, with an optional resolution_note). Resolving doesn't resume the AI; POST /ai/conversations/{id}/resume does. A resolved handoff can't change (409 handoff_resolved).

Example
Request body
{
  "assigned_user_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "resolved": true,
  "resolution_note": "Called back and agreed the group rate."
}
Response · 200 OK
{
  "id": "hof_01j9s4g3j5m7p9r1t3v5x7z9b1",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
  "reason": "The customer asks for a discount for a group of 40.",
  "trigger": "PRICING_EXCEPTION",
  "ai_state": { "stage": "negotiation", "intent": "group_booking" },
  "recommended_action": "Offer the group rate from the price list, or call back.",
  "execution_id": null,
  "created_by": null,
  "assigned_user": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "resolved_at": "2026-09-26T09:30:00Z",
  "resolved_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "resolution_note": "Called back and agreed the group rate.",
  "created_at": "2026-09-26T09:20:00Z",
  "updated_at": "2026-09-26T09:20:00Z",
  "conversation_title": "Aziza Karimova",
  "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9"
}
POST/ai/conversations/{id}/drafts/{draft}/approveai:operate

Send a draft the AI Seller prepared (autonomy 2, or 3 outside the approved scenarios). It goes out as the AI's message with you as sent_by, through the same consent gate and pacing as every reply; the first AI message of a conversation gets the business's disclosure line when disclosure is on. 409 draft_not_pending when it was approved, discarded or replaced by a newer draft (the customer wrote again); 409 recommendation_not_sendable for recommendations (autonomy 1: guidance, write the reply yourself); 409 reply_not_allowed when the gate refuses (the draft then stays pending). Needs an accepted acceptable-use policy.

Example
Response · 202 Accepted
{
  "draft": {
    "id": "sdr_01j9s4h4k6n8q0s2u4w6y8a0c2",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
    "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
    "execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
    "kind": "DRAFT",
    "status": "APPROVED",
    "body_len": 92,
    "language": "uz",
    "actions": ["ANSWER_QUESTION"],
    "validation": {
      "attempts": 1,
      "passed": true,
      "disclosure": "added",
      "failures": [],
      "warnings": [],
      "checks": [
        { "name": "honesty", "passed": true },
        { "name": "grounding", "passed": true },
        { "name": "language", "passed": true }
      ]
    },
    "reply_to_message_id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
    "message_id": "msg_01j9s4f2h4k6n8q0s2u4w6y8a0",
    "decided_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "decided_at": "2026-09-26T09:30:00Z",
    "created_at": "2026-09-26T09:14:11Z",
    "updated_at": "2026-09-26T09:14:11Z",
    "body": "Bu xabarga AI yordamchi javob bermoqda.\n\nSalom! Chipta narxi 150 000 soʻm. Qaysi kunga kerak?"
  },
  "message": {
    "id": "msg_01j9s4f2h4k6n8q0s2u4w6y8a0",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
    "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
    "telegram_message_id": null,
    "external_message_id": null,
    "direction": "OUT",
    "author": "AI",
    "body_len": 92,
    "media_kind": null,
    "execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
    "sent_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "status": "QUEUED",
    "send_basis": "REPLY_WINDOW",
    "attempts": 0,
    "error_code": null,
    "error": null,
    "occurred_at": "2026-09-26T09:30:00Z",
    "sent_at": null,
    "edited_at": null,
    "deleted_at": null,
    "imported": false,
    "read_at": null,
    "reaction": null,
    "metadata": null,
    "created_at": "2026-09-26T09:30:00Z",
    "updated_at": "2026-09-26T09:30:00Z",
    "body": "Bu xabarga AI yordamchi javob bermoqda.\n\nSalom! Chipta narxi 150 000 soʻm. Qaysi kunga kerak?"
  }
}
POST/ai/conversations/{id}/drafts/{draft}/discardai:operate

Set a pending draft or recommendation aside (status DISCARDED). 409 draft_not_pending when it isn't pending anymore.

Example
Response · 200 OK
{
  "draft": {
    "id": "sdr_01j9s4h4k6n8q0s2u4w6y8a0c2",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
    "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
    "execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
    "kind": "DRAFT",
    "status": "DISCARDED",
    "body_len": 92,
    "language": "uz",
    "actions": ["ANSWER_QUESTION"],
    "validation": {
      "attempts": 1,
      "passed": true,
      "disclosure": "added",
      "failures": [],
      "warnings": [],
      "checks": [
        { "name": "honesty", "passed": true },
        { "name": "grounding", "passed": true },
        { "name": "language", "passed": true }
      ]
    },
    "reply_to_message_id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
    "message_id": null,
    "decided_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "decided_at": "2026-09-26T09:30:00Z",
    "created_at": "2026-09-26T09:14:11Z",
    "updated_at": "2026-09-26T09:14:11Z",
    "body": "Bu xabarga AI yordamchi javob bermoqda.\n\nSalom! Chipta narxi 150 000 soʻm. Qaysi kunga kerak?"
  },
  "message": null
}
GET/ai/leadsai:read

The AI Seller's leads, the most recently updated first: one per conversation, opened when the customer shows interest, with the funnel stage (and its name), quality (cold, warm, hot), interest, status (OPEN; WON at the funnel's last stage; LOST when the customer opted out) and a value estimate from the prices of the offers the customer is interested in. Filter by business_id, stage (a stage key), quality, status, conversation_id and q (the customer's name).

Query: business_id, stage, quality, status, conversation_id, q, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "led_01j9s4j5m7p9r1t3v5x7z9b1d3",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
      "recipient_id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
      "title": "Aziza Karimova",
      "stage": "OFFER_PRESENTED",
      "quality": "warm",
      "interest": "high",
      "status": "OPEN",
      "value_estimate": 150000,
      "currency": "UZS",
      "offer_ids": ["ofr_01j9s4m7p9r1t3v5x7z9b1d3f5"],
      "owner_user_id": null,
      "last_execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
      "closed_at": null,
      "created_at": "2026-09-26T09:14:11Z",
      "updated_at": "2026-09-26T09:14:11Z",
      "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "stage_name": "Offer Presented"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/ai/sales-pipelineai:read

Funnel snapshot: for each business's default funnel, counts of open/won/lost leads per stage with estimated total value (sum of value_estimate for open leads) and oldest open lead age in days. Stage-to-stage conversion rates (derived). Filter by business_id. Each pipelineResult has kind 'mixed'; conversion rates have kind 'derived'; value_total is kind 'estimated' (from AI's value_estimate); counts are kind 'observed'.

Query: business_id

Example
Response · 200 OK
[
  {
    "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
    "funnel_id": "fun_01",
    "funnel_name": "Default",
    "total_open": 12,
    "total_won": 4,
    "total_lost": 2,
    "total_value": 5200000,
    "kind": "mixed",
    "stages": [
      {
        "stage_key": "AWARENESS",
        "stage_name": "Awareness",
        "position": 1,
        "lead_count": 5,
        "won_count": 0,
        "lost_count": 0,
        "value_total": 1200000,
        "oldest_days": 3.2
      }
    ],
    "conversions": [
      {
        "from": "AWARENESS",
        "to": "INTEREST",
        "leads_in": 12,
        "leads_through": 8,
        "rate": 66.7,
        "kind": "derived"
      }
    ]
  }
]
GET/ai/analyticsai:read

Conversation and sales metrics over a time range (from/to: YYYY-MM-DD or RFC3339, default last 30 days). Each metric carries kind: observed (counted from rows) | estimated (from AI judgements) | derived (computed ratio or average) and a filter_hint pointing at the underlying rows. Metrics: conversations_started, customers_replied, ai_turns_by_action (by final_action), handoffs_by_trigger, opt_outs, leads (opened/won/lost), avg_first_reply_seconds, stage_movements. Filter by business_id.

Query: business_id, from, to

Example
Response · 200 OK
{
  "from": "2026-09-01T00:00:00Z",
  "to": "2026-09-28T00:00:00Z",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "metrics": [
    {
      "key": "conversations_started",
      "value": 42,
      "kind": "observed",
      "filter_hint": "conversations.created_at in range"
    },
    {
      "key": "leads",
      "value": { "opened": 18, "won": 5, "lost": 2 },
      "kind": "observed",
      "filter_hint": "sales_leads.created_at / closed_at in range"
    },
    {
      "key": "avg_first_reply_seconds",
      "value": 8,
      "kind": "derived",
      "filter_hint": "conversation_messages, first IN then first OUT in range"
    }
  ]
}
GET/ai/performanceai:read

Per-agent/business/model performance over a time range (from/to: YYYY-MM-DD or RFC3339, default last 30 days). Counts runs, succeeded, failed, blocked; average latency (derived); total cost_micros; fallbacks (JEV → LLM fallback count); jev_runs (runs with a JEV decision). Validator failure codes with counts. All from ai_executions (mode=LIVE).

Query: from, to

Example
Response · 200 OK
{
  "from": "2026-09-01T00:00:00Z",
  "to": "2026-09-28T00:00:00Z",
  "rows": [
    {
      "agent": "SELLER",
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "model": "gpt-4o-mini",
      "runs": 150,
      "succeeded": 142,
      "failed": 3,
      "blocked": 5,
      "avg_latency_ms": 1240,
      "cost_micros": 45000,
      "fallbacks": 12,
      "jev_runs": 138,
      "input_tokens": 210000,
      "output_tokens": 18000,
      "kind": "mixed"
    }
  ],
  "validator_failures": [
    { "check": "honesty", "count": 2, "kind": "observed" }
  ]
}

AI Seller: knowledge and memory#

What the AI Seller knows about a business and remembers about its customers. Knowledge sources are text, FAQ entries, offers (synced from offers), files and web pages; each change is ingested in the background into a new version (extracted, split into chunks, embedded when an embedding provider is configured), and the Seller only ever reads a source's published version. Memories are sealed customer data. Reading needs ai:read; changes need ai:manage and are audited.

GET/ai/knowledge/sourcesai:read

Knowledge sources, newest first. version is the published version the Seller reads (0 until the first ingest finishes). status is PENDING or PROCESSING while a new version is built, READY, or FAILED with error; the last good version keeps answering meanwhile. embedding_model null means full-text search only. Lists leave out config.text and config.items: get the source for them.

Query: business_id, kind, status, q, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "kns_01j9s3m5p7r9t1v3x5z7b9d1f3",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "kind": "FAQ",
      "title": "Yetkazib berish va to‘lov",
      "status": "READY",
      "version": 3,
      "config": {},
      "chunk_count": 1,
      "token_count": 84,
      "embedding_model": "text-embedding-3-small",
      "last_ingested_at": "2026-09-26T09:30:00Z",
      "error": null,
      "warning": null,
      "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "created_at": "2026-09-20T08:00:00Z",
      "updated_at": "2026-09-26T09:30:00Z"
    },
    {
      "id": "kns_01j9s3q1s3u5w7y9a1c3e5g7j9",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "kind": "URL",
      "title": "Narxlar — Silk Road Events",
      "status": "READY",
      "version": 1,
      "config": { "url": "https://silkroad.example/narxlar" },
      "chunk_count": 4,
      "token_count": 2210,
      "embedding_model": "text-embedding-3-small",
      "last_ingested_at": "2026-09-26T09:30:00Z",
      "error": null,
      "warning": null,
      "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "created_at": "2026-09-20T08:00:00Z",
      "updated_at": "2026-09-26T09:30:00Z"
    }
  ],
  "total": 2,
  "limit": 50,
  "offset": 0
}
POST/ai/knowledge/sourcesai:manage

Add knowledge to a business: kind TEXT (title, text up to 200,000 characters), FAQ (title, items: up to 500 questions with answers), URL (url of a public page, fetched through the same address guard as custom AI providers; the title defaults to the page's) or FILE (attachment_id of an uploaded attachment). The source starts PENDING and is ingested in the background. OFFER sources come from offers and can't be added here.

To upload a file, send multipart/form-data with business_id, an optional title and file: .txt, .md, .csv, .tsv or .html, UTF-8 (UTF-16 with a byte-order mark and Windows-1251 are read too), up to 5 MB. PDF and Word files are refused with 422. A business can have 500 sources (409 knowledge_source_limit); an organization can start 200 ingests (additions, edits, reindexes) an hour (429 ai_rate_limited).

Example
Request body
{
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "kind": "FAQ",
  "title": "Yetkazib berish va to‘lov",
  "items": [
    {
      "question": "Yetkazib berish bormi?",
      "answer": "Ha, Toshkent bo‘ylab ertasi kuni, 30 000 so‘m. 500 000 so‘mdan ortiq buyurtmalarga bepul."
    },
    {
      "question": "Qanday to‘lash mumkin?",
      "answer": "Click, Payme, karta yoki naqd pul bilan."
    }
  ]
}
Response · 201 Created
{
  "id": "kns_01j9s3m5p7r9t1v3x5z7b9d1f3",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "kind": "FAQ",
  "title": "Yetkazib berish va to‘lov",
  "status": "PENDING",
  "version": 0,
  "config": {
    "items": [
      {
        "question": "Yetkazib berish bormi?",
        "answer": "Ha, Toshkent bo‘ylab ertasi kuni, 30 000 so‘m. 500 000 so‘mdan ortiq buyurtmalarga bepul."
      },
      {
        "question": "Qanday to‘lash mumkin?",
        "answer": "Click, Payme, karta yoki naqd pul bilan."
      }
    ]
  },
  "chunk_count": 0,
  "token_count": 0,
  "embedding_model": null,
  "last_ingested_at": null,
  "error": null,
  "warning": null,
  "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "created_at": "2026-09-20T08:00:00Z",
  "updated_at": "2026-09-26T09:30:00Z"
}
GET/ai/knowledge/sources/{id}ai:read

A knowledge source with its full config: the text, the FAQ items, the page address or the file.

Example
Response · 200 OK
{
  "id": "kns_01j9s3m5p7r9t1v3x5z7b9d1f3",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "kind": "FAQ",
  "title": "Yetkazib berish va to‘lov",
  "status": "READY",
  "version": 3,
  "config": {
    "items": [
      {
        "question": "Yetkazib berish bormi?",
        "answer": "Ha, Toshkent bo‘ylab ertasi kuni, 30 000 so‘m. 500 000 so‘mdan ortiq buyurtmalarga bepul."
      },
      {
        "question": "Qanday to‘lash mumkin?",
        "answer": "Click, Payme, karta yoki naqd pul bilan."
      }
    ]
  },
  "chunk_count": 1,
  "token_count": 84,
  "embedding_model": "text-embedding-3-small",
  "last_ingested_at": "2026-09-26T09:30:00Z",
  "error": null,
  "warning": null,
  "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "created_at": "2026-09-20T08:00:00Z",
  "updated_at": "2026-09-26T09:30:00Z"
}
PATCH/ai/knowledge/sources/{id}ai:manage

Change a source: title, text (TEXT), items (FAQ, the whole list) or url (URL). A change queues a new version, and the current one keeps answering until it's ready. kind, business_id and the file can't change: add a new source instead. Offer sources follow their offer (409 knowledge_source_managed).

Example
Request body
{
  "items": [
    {
      "question": "Yetkazib berish bormi?",
      "answer": "Ha, Toshkent bo‘ylab ertasi kuni, 30 000 so‘m. 500 000 so‘mdan ortiq buyurtmalarga bepul."
    },
    {
      "question": "Qanday to‘lash mumkin?",
      "answer": "Click, Payme, karta yoki naqd pul bilan."
    },
    {
      "question": "Qaytarish mumkinmi?",
      "answer": "14 kun ichida, chek bilan."
    }
  ]
}
Response · 200 OK
{
  "id": "kns_01j9s3m5p7r9t1v3x5z7b9d1f3",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "kind": "FAQ",
  "title": "Yetkazib berish va to‘lov",
  "status": "PENDING",
  "version": 3,
  "config": {
    "items": [
      {
        "question": "Yetkazib berish bormi?",
        "answer": "Ha, Toshkent bo‘ylab ertasi kuni, 30 000 so‘m. 500 000 so‘mdan ortiq buyurtmalarga bepul."
      },
      {
        "question": "Qanday to‘lash mumkin?",
        "answer": "Click, Payme, karta yoki naqd pul bilan."
      }
    ]
  },
  "chunk_count": 1,
  "token_count": 84,
  "embedding_model": "text-embedding-3-small",
  "last_ingested_at": "2026-09-26T09:30:00Z",
  "error": null,
  "warning": null,
  "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "created_at": "2026-09-20T08:00:00Z",
  "updated_at": "2026-09-26T09:30:00Z"
}
DELETE/ai/knowledge/sources/{id}ai:manage

Delete a source with every version of it. A file uploaded for it is deleted too, unless a campaign uses it.

Example

Response · 204 No Content, no body

POST/ai/knowledge/sources/{id}/reindexai:manage

Build a new version of a source: its text is chunked again, a page fetched again, and everything embedded with the current embedding model. Answers 202 with the source, now PENDING; it counts toward the hourly ingest limit.

Example
Response · 202 Accepted
{
  "id": "kns_01j9s3q1s3u5w7y9a1c3e5g7j9",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "kind": "URL",
  "title": "Narxlar — Silk Road Events",
  "status": "PENDING",
  "version": 1,
  "config": { "url": "https://silkroad.example/narxlar" },
  "chunk_count": 4,
  "token_count": 2210,
  "embedding_model": "text-embedding-3-small",
  "last_ingested_at": "2026-09-26T09:30:00Z",
  "error": null,
  "warning": null,
  "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "created_at": "2026-09-20T08:00:00Z",
  "updated_at": "2026-09-26T09:30:00Z"
}
GET/ai/knowledge/searchai:read

Retrieve knowledge the way the Seller does, to test it: q is what a customer might write (up to 1,000 characters). Retrieval is hybrid: full-text matching of the query's words (oʻ, o' and o match, as do ё and е, and fuzzy matching catches word forms and typos) and, with an embedding provider, similarity of meaning. why says what matched: fts (words), vector (meaning only) or both. Hits fit within budget estimated tokens (default 2,000); limit is 1 to 50 (default 5).

Query: business_id, q, kind, limit, budget

mode is hybrid when the query was embedded, else full_text. strategy is exhaustive when every chunk of the business was scored (knowledge bases up to 256 chunks), prefilter when full-text matching chose the candidates. 600 searches per organization per hour.

Example
Response · 200 OK
{
  "data": [
    {
      "chunk_id": "knc_01j9s5p7r9t1v3x5z7b9d1f3h5",
      "source_id": "kns_01j9s3m5p7r9t1v3x5z7b9d1f3",
      "document_id": "knd_01j9s4n6q8s0u2w4y6a8c0e2g4",
      "kind": "FAQ",
      "version": 3,
      "title": "Yetkazib berish va to‘lov",
      "heading": "Yetkazib berish bormi?",
      "content": "## Yetkazib berish bormi?\n\nHa, Toshkent bo‘ylab ertasi kuni, 30 000 so‘m. 500 000 so‘mdan ortiq buyurtmalarga bepul.",
      "tokens": 44,
      "score": 0.8412,
      "why": "both",
      "lexical": 1,
      "vector": 0.7353
    }
  ],
  "mode": "hybrid",
  "strategy": "exhaustive",
  "embedding_model": "text-embedding-3-small",
  "words": ["yetkazib", "berish", "bormi"],
  "candidates": 7,
  "tokens": 44
}
GET/ai/memoryai:read

What the Seller remembers, most recently updated first: facts about customers (LONG_TERM), conversation notes (SHORT_TERM, MEDIUM_TERM), BUSINESS and SYSTEM memories, with importance, confidence and where each came from (source_type MESSAGE, EXECUTION, MANUAL or SYSTEM). status is ACTIVE by default; SUPERSEDED memories were replaced by a changed fact (superseded_by), EXPIRED ones are about to be deleted. type and status take several values separated by commas.

Query: business_id, recipient_id, telegram_user_id, conversation_id, type, status, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "mry_01j9s6q8s0u2w4y6a8c0e2g4j6",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "conversation_id": "cnv_01j9s7r9t1v3x5z7b9d1f3h5k7",
      "recipient_id": "rcp_01j9r5b3d5f7h9k1m3p5r7t9v1",
      "telegram_user_id": 5712093344,
      "type": "LONG_TERM",
      "key": "budget",
      "content": "Byudjeti taxminan 5 million so‘m.",
      "importance": 0.8,
      "confidence": 0.9,
      "source_type": "EXECUTION",
      "source_id": "aix_01j9s8s0u2w4y6a8c0e2g4j6m8",
      "embedding_model": null,
      "expires_at": null,
      "superseded_by": null,
      "superseded_at": null,
      "status": "ACTIVE",
      "created_at": "2026-09-25T14:02:11Z",
      "updated_at": "2026-09-26T09:30:00Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
DELETE/ai/memory/{id}ai:manage

Forget a memory for good, with the older statements of the same fact it replaced: for a privacy request or a wrong fact. The deletion is kept in the audit log and the memory's history, without the content.

Example

Response · 204 No Content, no body

POST/ai/researchai:manage

Start a research job for a business subject (company name + optional website). The agent fetches the website, optionally queries a configured search provider, then calls the LLM to produce a structured report. Every fact cites an evidence URL that was actually fetched. The agent refuses private individuals (422 private_individual). Rate-limited to 20 per organization per hour. While the platform's research switch (ai_research_enabled, or AI as a whole) is off, nothing is created: 409 research_disabled. A job queued before the switch went off ends FAILED with the reason in error_message instead of staying PENDING. Audited.

Example
Request body
{
  "subject_name": "Nexgen Solutions",
  "subject_website": "https://nexgen-solutions.example.com",
  "business_id": "biz_01j9..."
}
Response · 200 OK
{
  "id": "rsj_01j9...",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "subject_name": "Nexgen Solutions",
  "subject_website": "https://nexgen-solutions.example.com",
  "status": "PENDING",
  "requested_by": "usr_01j9...",
  "created_at": "2026-09-27T12:00:00Z",
  "updated_at": "2026-09-27T12:00:00Z"
}
GET/ai/researchai:read

List research jobs for the organization, newest first. Supports ?limit=, ?offset= and ?status= (PENDING | RUNNING | SUCCEEDED | FAILED).

Example
Response · 200 OK
{
  "data": [
    {
      "id": "rsj_01j9...",
      "subject_name": "Nexgen Solutions",
      "status": "SUCCEEDED",
      "created_at": "2026-09-27T12:00:00Z"
    }
  ],
  "total": 1,
  "limit": 25,
  "offset": 0
}
GET/ai/research/{id}ai:read

Get a research job and its report (when status is SUCCEEDED). The report contains a summary, a list of facts each with an evidence URL and confidence score, and a retrieved_at timestamp. Returns job even while PENDING or RUNNING.

Example
Response · 200 OK
{
  "job": {
    "id": "rsj_01j9...",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "subject_name": "Nexgen Solutions",
    "status": "SUCCEEDED",
    "created_at": "2026-09-27T12:00:00Z",
    "completed_at": "2026-09-27T12:02:30Z"
  },
  "report": {
    "summary": "Nexgen Solutions is a B2B software company founded in 2018, specializing in ERP integrations for mid-market manufacturers in Central Asia.",
    "facts": [
      {
        "claim": "Founded in 2018",
        "evidence_url": "https://nexgen-solutions.example.com/about",
        "confidence": 0.95
      },
      {
        "claim": "Serves 120+ clients in Uzbekistan and Kazakhstan",
        "evidence_url": "https://nexgen-solutions.example.com/customers",
        "confidence": 0.87
      }
    ],
    "retrieved_at": "2026-09-27T12:02:25Z"
  }
}
POST/ai/research/{id}/save-to-knowledgeai:manage

Save a completed research report as a TEXT knowledge source linked to a business. The report's summary and facts are formatted as readable text. Requires the job to be SUCCEEDED. Audited.

Example
Request body
{
  "business_id": "biz_01j9...",
  "title": "Research: Nexgen Solutions"
}
Response · 200 OK
{
  "id": "ks_01j9...",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "business_id": "biz_01j9...",
  "kind": "TEXT",
  "title": "Research: Nexgen Solutions",
  "status": "PENDING",
  "created_at": "2026-09-27T12:03:00Z"
}

AI Seller: decisions#

The AI Seller's decision layer. For each customer message, TypeSafe's System One model (JEV) answers one batch of typed questions: intent, sales stage, whether to advance it, next action, interest, lead quality, objection, and whether to hand off, book a time, look up knowledge, stop messaging or say it's an AI. It returns probabilities and a confidence per field; it never writes the reply. Fields below their confidence threshold, or all of them when JEV is off, has no key or fails, go to your AI model with the same questions, and each field records its source (JEV or LLM). JEV is opt-in: with it on, each customer message (card numbers, phone numbers and e-mail addresses masked), a short conversation summary, the sales stage and funnel objectives, known needs and objections, offer names and the language go to TypeSafe. Never the conversation history, customer profiles, Telegram ids or keys. Decisions store the questions, answers, probabilities, confidence, thresholds and sources, not the message and no reasoning.

GET/ai/jevai:read

JEV settings: whether it's on, has_key and key_hint (the key itself is never returned), key_source (the key decisions use now: ORGANIZATION, PLATFORM when the platform provides one, or NONE), the effective per-field confidence thresholds with the defaults and your overrides, when data sharing was accepted, and disclosure, which lists what goes to TypeSafe.

Example
Response · 200 OK
{
  "enabled": true,
  "has_key": true,
  "key_hint": "•••• 7c1e",
  "key_source": "ORGANIZATION",
  "platform_key_available": false,
  "thresholds": {
    "intent": 0.5,
    "stage": 0.5,
    "advance_stage": 0.6,
    "next_action": 0.7,
    "interest": 0.5,
    "lead_quality": 0.5,
    "objection": 0.5,
    "human_handoff": 0.6,
    "calendar_intent": 0.6,
    "needs_knowledge": 0.5,
    "opt_out_request": 0.6,
    "is_asking_if_ai": 0.6
  },
  "default_thresholds": {
    "intent": 0.5,
    "stage": 0.5,
    "advance_stage": 0.6,
    "next_action": 0.5,
    "interest": 0.5,
    "lead_quality": 0.5,
    "objection": 0.5,
    "human_handoff": 0.6,
    "calendar_intent": 0.6,
    "needs_knowledge": 0.5,
    "opt_out_request": 0.6,
    "is_asking_if_ai": 0.6
  },
  "threshold_overrides": { "next_action": 0.7 },
  "data_sharing_accepted_at": "2026-09-27T08:12:00Z",
  "data_sharing_accepted_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "model": "jev-latest",
  "disclosure": {
    "processor": "TypeSafe (System One)",
    "endpoint": "https://api.typesafe.ai/v1/systemone",
    "sent": [
      "latest_customer_message",
      "conversation_summary",
      "current_and_next_stage",
      "funnel_stage_objectives",
      "allowed_actions",
      "known_needs",
      "known_objections",
      "offer_names",
      "language",
      "decision_questions"
    ],
    "masked_before_sending": [
      "card_numbers",
      "phone_numbers",
      "email_addresses",
      "key_like_strings"
    ],
    "never_sent": [
      "conversation_history",
      "customer_profile",
      "telegram_ids",
      "organization_and_business_ids",
      "api_keys_and_secrets"
    ]
  },
  "updated_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "updated_at": "2026-09-26T09:30:00Z"
}
PUT/ai/jevai:manage

Change the settings; fields you leave out keep their value. Turning JEV on needs accept_data_sharing: true once, which records who accepted and when. api_key: a new TypeSafe key replaces the saved one, "" keeps it, null removes it (decisions then use the platform key, if provided). thresholds replaces your overrides: numbers from 0 to 1 per field; a field left out or null uses its default, and null for the whole object clears them. A choice or score's confidence is JEV's own; a yes/no field's is its distance from 0.5, doubled, so 0.6 needs a probability of at least 0.8 or at most 0.2.

Audited as AI_JEV_SETTINGS_UPDATED, without the key. Turning JEV on without accepting data sharing is 422 with details.accept_data_sharing.

Example
Request body
{
  "enabled": true,
  "accept_data_sharing": true,
  "api_key": "ts-…",
  "thresholds": { "next_action": 0.7 }
}
Response · 200 OK
{
  "enabled": true,
  "has_key": true,
  "key_hint": "•••• 7c1e",
  "key_source": "ORGANIZATION",
  "platform_key_available": false,
  "thresholds": {
    "intent": 0.5,
    "stage": 0.5,
    "advance_stage": 0.6,
    "next_action": 0.7,
    "interest": 0.5,
    "lead_quality": 0.5,
    "objection": 0.5,
    "human_handoff": 0.6,
    "calendar_intent": 0.6,
    "needs_knowledge": 0.5,
    "opt_out_request": 0.6,
    "is_asking_if_ai": 0.6
  },
  "default_thresholds": {
    "intent": 0.5,
    "stage": 0.5,
    "advance_stage": 0.6,
    "next_action": 0.5,
    "interest": 0.5,
    "lead_quality": 0.5,
    "objection": 0.5,
    "human_handoff": 0.6,
    "calendar_intent": 0.6,
    "needs_knowledge": 0.5,
    "opt_out_request": 0.6,
    "is_asking_if_ai": 0.6
  },
  "threshold_overrides": { "next_action": 0.7 },
  "data_sharing_accepted_at": "2026-09-27T08:12:00Z",
  "data_sharing_accepted_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "model": "jev-latest",
  "disclosure": {
    "processor": "TypeSafe (System One)",
    "endpoint": "https://api.typesafe.ai/v1/systemone",
    "sent": [
      "latest_customer_message",
      "conversation_summary",
      "current_and_next_stage",
      "funnel_stage_objectives",
      "allowed_actions",
      "known_needs",
      "known_objections",
      "offer_names",
      "language",
      "decision_questions"
    ],
    "masked_before_sending": [
      "card_numbers",
      "phone_numbers",
      "email_addresses",
      "key_like_strings"
    ],
    "never_sent": [
      "conversation_history",
      "customer_profile",
      "telegram_ids",
      "organization_and_business_ids",
      "api_keys_and_secrets"
    ]
  },
  "updated_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "updated_at": "2026-09-26T09:30:00Z"
}
POST/ai/jev/testai:manage

Check a key with one decision on a synthetic customer message ("Salom, narxi qancha? Yetkazib berish bormi?") and a sample funnel: JEV only, whether or not JEV is on, since nothing of yours leaves Oqim. Uses the saved key, else the platform's; send api_key to try one before saving it. A failed check is still a 200: ok is false and error has the code (auth_failed, rate_limited, timeout, …) and message. The decision is stored in TEST mode.

No key to test is 409 jev_no_key. Counts toward the organization's 60 AI checks an hour (429 ai_rate_limited).

Example
Request body
{
  "api_key": "ts-…"
}
Response · 200 OK
{
  "ok": true,
  "key_source": "ORGANIZATION",
  "model": "jev-1.13.0",
  "latency_ms": 1416,
  "error": null,
  "decision": {
    "id": "jvd_01j9rs4w6y8a0c2e4g6j8m0p2r",
    "source": "JEV",
    "intent": {
      "value": "pricing_question",
      "probabilities": {
        "pricing_question": 1,
        "human_request": 0,
        "delivery": 0,
        "product_question": 0,
        "other": 0
      },
      "confidence": 1,
      "threshold": 0.5,
      "trusted": true,
      "source": "JEV"
    },
    "stage": {
      "value": "discovery",
      "probabilities": { "greeting": 0, "discovery": 0.92, "offer": 0.08, "closing": 0 },
      "confidence": 0.89,
      "threshold": 0.5,
      "trusted": true,
      "source": "JEV"
    },
    "advance_stage": {
      "value": false,
      "probability": 0.29,
      "confidence": 0.42,
      "threshold": 0.6,
      "trusted": false,
      "source": "JEV"
    },
    "next_action": {
      "value": "ANSWER_QUESTION",
      "probabilities": {
        "ANSWER_QUESTION": 0.7,
        "ASK_DISCOVERY": 0.18,
        "PRESENT_OFFER": 0.1,
        "HANDOFF": 0.02
      },
      "confidence": 0.65,
      "threshold": 0.7,
      "trusted": false,
      "source": "JEV"
    },
    "interest": {
      "value": 4,
      "level": "high",
      "levels": 5,
      "probabilities": { "none": 0, "low": 0, "moderate": 0, "high": 1, "very_high": 0 },
      "confidence": 1,
      "threshold": 0.5,
      "trusted": true,
      "source": "JEV"
    },
    "lead_quality": {
      "value": 2.06,
      "level": "warm",
      "levels": 3,
      "probabilities": { "cold": 0, "warm": 0.94, "hot": 0.06 },
      "confidence": 0.92,
      "threshold": 0.5,
      "trusted": true,
      "source": "JEV"
    },
    "objection": {
      "value": "none",
      "probabilities": { "none": 0.87, "price": 0.12, "other": 0.01 },
      "confidence": 0.86,
      "threshold": 0.5,
      "trusted": true,
      "source": "JEV"
    },
    "human_handoff": {
      "value": false,
      "probability": 0.04,
      "confidence": 0.92,
      "threshold": 0.6,
      "trusted": true,
      "source": "JEV"
    },
    "calendar_intent": {
      "value": false,
      "probability": 0.02,
      "confidence": 0.96,
      "threshold": 0.6,
      "trusted": true,
      "source": "JEV"
    },
    "needs_knowledge": {
      "value": true,
      "probability": 0.95,
      "confidence": 0.9,
      "threshold": 0.5,
      "trusted": true,
      "source": "JEV"
    },
    "opt_out_request": {
      "value": false,
      "probability": 0.01,
      "confidence": 0.98,
      "threshold": 0.6,
      "trusted": true,
      "source": "JEV"
    },
    "is_asking_if_ai": {
      "value": false,
      "probability": 0.01,
      "confidence": 0.98,
      "threshold": 0.6,
      "trusted": true,
      "source": "JEV"
    },
    "jev_status": "OK",
    "key_source": "ORGANIZATION",
    "model": "jev-1.13.0",
    "latency_ms": 1416
  }
}
GET/ai/jev/decisions/{id}ai:read

One stored decision: its mode (LIVE, PLAYGROUND, EVAL or TEST), source (JEV, LLM, MIXED or NONE), jev_status (OK, ERROR, DISABLED or NO_KEY), the questions asked, the answers (JEV's as received, the fallback model's normalized), the decision with every field's value, probabilities, confidence, threshold, trusted flag and source, the thresholds applied, the input's sizes (never its text), latency and tokens.

Example
Response · 200 OK
{
  "id": "jvd_01j9rs4w6y8a0c2e4g6j8m0p2r",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "business_id": null,
  "execution_id": null,
  "conversation_id": null,
  "mode": "TEST",
  "source": "JEV",
  "jev_status": "OK",
  "key_source": "ORGANIZATION",
  "model": "jev-1.13.0",
  "questions": {
    "intent": {
      "type": "choice",
      "instructions": "What does the customer mainly want with their latest message? …",
      "criteria": {
        "pricing_question": "Asks about price, cost, discounts, payment terms or installments"
      }
    },
    "advance_stage": {
      "type": "noul",
      "instructions": "The customer's latest message meets the objective of the current stage …",
      "criteria": { "true": "…", "false": "…" }
    }
  },
  "answers": {
    "jev": {
      "intent": {
        "type": "choice",
        "choice": "pricing_question",
        "probabilities": { "pricing_question": 1, "delivery": 0 },
        "confidence": 1
      },
      "interest": {
        "type": "score",
        "score": 3,
        "legend": { "0": "None: …", "3": "High: …", "4": "Very high: …" },
        "probabilities": { "0": 0, "3": 1, "4": 0 },
        "confidence": 1
      }
    }
  },
  "decision": {
    "id": "jvd_01j9rs4w6y8a0c2e4g6j8m0p2r",
    "source": "JEV",
    "intent": {
      "value": "pricing_question",
      "probabilities": {
        "pricing_question": 1,
        "human_request": 0,
        "delivery": 0,
        "product_question": 0,
        "other": 0
      },
      "confidence": 1,
      "threshold": 0.5,
      "trusted": true,
      "source": "JEV"
    },
    "stage": {
      "value": "discovery",
      "probabilities": { "greeting": 0, "discovery": 0.92, "offer": 0.08, "closing": 0 },
      "confidence": 0.89,
      "threshold": 0.5,
      "trusted": true,
      "source": "JEV"
    },
    "advance_stage": {
      "value": false,
      "probability": 0.29,
      "confidence": 0.42,
      "threshold": 0.6,
      "trusted": false,
      "source": "JEV"
    },
    "next_action": {
      "value": "ANSWER_QUESTION",
      "probabilities": {
        "ANSWER_QUESTION": 0.7,
        "ASK_DISCOVERY": 0.18,
        "PRESENT_OFFER": 0.1,
        "HANDOFF": 0.02
      },
      "confidence": 0.65,
      "threshold": 0.7,
      "trusted": false,
      "source": "JEV"
    },
    "interest": {
      "value": 4,
      "level": "high",
      "levels": 5,
      "probabilities": { "none": 0, "low": 0, "moderate": 0, "high": 1, "very_high": 0 },
      "confidence": 1,
      "threshold": 0.5,
      "trusted": true,
      "source": "JEV"
    },
    "lead_quality": {
      "value": 2.06,
      "level": "warm",
      "levels": 3,
      "probabilities": { "cold": 0, "warm": 0.94, "hot": 0.06 },
      "confidence": 0.92,
      "threshold": 0.5,
      "trusted": true,
      "source": "JEV"
    },
    "objection": {
      "value": "none",
      "probabilities": { "none": 0.87, "price": 0.12, "other": 0.01 },
      "confidence": 0.86,
      "threshold": 0.5,
      "trusted": true,
      "source": "JEV"
    },
    "human_handoff": {
      "value": false,
      "probability": 0.04,
      "confidence": 0.92,
      "threshold": 0.6,
      "trusted": true,
      "source": "JEV"
    },
    "calendar_intent": {
      "value": false,
      "probability": 0.02,
      "confidence": 0.96,
      "threshold": 0.6,
      "trusted": true,
      "source": "JEV"
    },
    "needs_knowledge": {
      "value": true,
      "probability": 0.95,
      "confidence": 0.9,
      "threshold": 0.5,
      "trusted": true,
      "source": "JEV"
    },
    "opt_out_request": {
      "value": false,
      "probability": 0.01,
      "confidence": 0.98,
      "threshold": 0.6,
      "trusted": true,
      "source": "JEV"
    },
    "is_asking_if_ai": {
      "value": false,
      "probability": 0.01,
      "confidence": 0.98,
      "threshold": 0.6,
      "trusted": true,
      "source": "JEV"
    },
    "jev_status": "OK",
    "key_source": "ORGANIZATION",
    "model": "jev-1.13.0",
    "latency_ms": 1416
  },
  "thresholds": {
    "intent": 0.5,
    "stage": 0.5,
    "advance_stage": 0.6,
    "next_action": 0.7,
    "interest": 0.5,
    "lead_quality": 0.5,
    "objection": 0.5,
    "human_handoff": 0.6,
    "calendar_intent": 0.6,
    "needs_knowledge": 0.5,
    "opt_out_request": 0.6,
    "is_asking_if_ai": 0.6
  },
  "input": {
    "message_chars": 43,
    "summary_chars": 0,
    "current_stage": "discovery",
    "stages": 4,
    "allowed_actions": 3,
    "needs": 1,
    "objections": 0,
    "offers": 2,
    "language": "uz",
    "masked": 0,
    "truncated": false
  },
  "fallback_fields": [],
  "llm_provider_id": null,
  "llm_model": null,
  "latency_ms": 1416,
  "jev_latency_ms": 1416,
  "llm_latency_ms": null,
  "jev_input_tokens": 2233,
  "jev_output_tokens": 471,
  "llm_input_tokens": 0,
  "llm_output_tokens": 0,
  "error": null,
  "created_at": "2026-09-26T09:30:00Z"
}

AI Seller: quality#

Feedback, evaluation datasets and items, evaluation runs, and A/B experiments for the AI Seller. Feedback captures human annotations on AI replies (labels, rating, optional corrected text), with optional review requests. Datasets hold replay items; evaluations run those items through the current or a candidate config and score each reply with JEV or an LLM judge; experiments compare variants. Reading needs ai:read; adding feedback and items needs ai:operate; everything else needs ai:manage.

POST/ai/feedbackai:operate

Submit feedback on an AI reply: optional execution_id, conversation_id, message_id, labels (GOOD, WRONG_FACT, WRONG_TONE, TOO_LONG, MISSED_OPPORTUNITY, SHOULD_HANDOFF, UNSAFE, OTHER), rating (1–5), tags, explanation, corrected_reply, suggested_response. Set request_review: true to request a human review immediately.

Example
Request body
{
  "execution_id": "aix_01j9...",
  "labels": ["WRONG_FACT"],
  "rating": 2,
  "explanation": "The price is 120,000, not 150,000.",
  "corrected_reply": "Our price is 120,000 UZS."
}
Response · 201 Created
{
  "id": "afb_01j9...",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "labels": ["WRONG_FACT"],
  "rating": 2,
  "tags": [],
  "review_requested_at": null,
  "created_at": "2026-09-27T10:00:00Z",
  "updated_at": "2026-09-27T10:00:00Z"
}
GET/ai/feedbackai:read

List feedback, most-recent first. Optional execution_id and conversation_id filters.

Query: execution_id, conversation_id, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "afb_01j9...",
      "labels": ["GOOD"],
      "rating": 5
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/ai/feedback/{id}ai:read

One feedback item. The explanation, corrected_reply and suggested_response fields are unsealed and returned in plain text.

Example
Response · 200 OK
{
  "id": "afb_01j9...",
  "labels": ["WRONG_FACT"],
  "rating": 2,
  "explanation": "The price is 120,000, not 150,000."
}
POST/ai/feedback/{id}/reviewai:operate

Mark this feedback item as review-requested and enqueue a review card task. Idempotent: a second call on an already-requested item is a no-op.

Example

Response · 204 No Content, no body

POST/ai/eval-datasetsai:manage

Create an evaluation dataset: name (required), optional business_id and description.

Example
Request body
{
  "name": "Seller v2 — price objections",
  "description": "Items where the customer asked about price.",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c"
}
Response · 201 Created
{
  "id": "eds_01j9...",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Seller v2 — price objections",
  "item_count": 0,
  "created_at": "2026-09-27T10:00:00Z"
}
GET/ai/eval-datasetsai:read

List evaluation datasets, most-recent first.

Query: limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "eds_01j9...",
      "name": "Seller v2 — price objections",
      "item_count": 12
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/ai/eval-datasets/{id}ai:read

One evaluation dataset.

Example
Response · 200 OK
{
  "id": "eds_01j9...",
  "name": "Seller v2 — price objections",
  "item_count": 12
}
DELETE/ai/eval-datasets/{id}ai:manage

Delete a dataset and all its items. Refused while an evaluation referencing it is RUNNING.

Example

Response · 204 No Content, no body

POST/ai/eval-datasets/{id}/itemsai:operate

Add one item to the dataset: source (MANUAL or FEEDBACK), optional feedback_id, meta (topic, notes, tags), conversation (list of {direction, author, text} snapshots), reference_reply (the expected reply to score candidates against). The conversation and reference_reply are sealed at rest.

Example
Request body
{
  "source": "MANUAL",
  "reference_reply": "Our standard delivery is 2–3 business days, 500 UZS.",
  "conversation": [
    {
      "direction": "IN",
      "author": "CUSTOMER",
      "text": "Yetkazib berish qancha turadi?"
    }
  ],
  "meta": {
    "topic": "delivery",
    "tags": ["price"]
  }
}
Response · 201 Created
{
  "id": "edi_01j9...",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "dataset_id": "eds_01j9...",
  "source": "MANUAL",
  "created_at": "2026-09-27T10:00:00Z"
}
GET/ai/eval-datasets/{id}/itemsai:read

List items in a dataset, oldest-first. The conversation and reference_reply are unsealed in the response.

Query: limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "edi_01j9...",
      "source": "MANUAL",
      "reference_reply": "Our standard delivery is 2–3 business days, 500 UZS."
    }
  ],
  "total": 12,
  "limit": 50,
  "offset": 0
}
POST/ai/evaluationsai:manage

Create an evaluation run: dataset_id and name (both required), optional business_id (else the dataset's business) and candidate_config: prompt_version_id (a Seller version of any status: DRAFT and TESTING ones are what you try out), communication_profile_id, language_profile_id, autonomy_level (0–5), and provider_id and model (every replay is pinned to exactly that provider and model, with no fallback; provider_id alone uses the provider's own model, model alone the provider the Seller's chain starts with). Missing keys fall back to the business's live configuration; candidate_config that isn't an object, an autonomy_level outside 0–5, a provider_id that isn't one of the organization's providers or a malformed model id is 422 (details name candidate_config.<key>). Status starts PENDING.

Example
Request body
{
  "name": "Seller v2 baseline",
  "dataset_id": "eds_01j9...",
  "candidate_config": { "prompt_version_id": "prv_01j9..." }
}
Response · 201 Created
{
  "id": "evl_01j9...",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Seller v2 baseline",
  "status": "PENDING",
  "items_total": 0,
  "items_scored": 0,
  "items_skipped": 0,
  "total_items": 0,
  "scored_items": 0,
  "skipped_items": 0,
  "avg_score": null,
  "pass_rate": null
}
GET/ai/evaluationsai:read

List evaluation runs, most-recent first.

Query: limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "evl_01j9...",
      "name": "Seller v2 baseline",
      "status": "SUCCEEDED",
      "items_total": 24,
      "items_scored": 22,
      "items_skipped": 2,
      "avg_score": 0.87,
      "pass_rate": 0.92
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/ai/evaluations/{id}ai:read

One evaluation run with all fields. items_total is items_scored + items_skipped; avg_score and pass_rate cover the scored items only (a skipped item has no reply to judge). The same three counters are also served as total_items, scored_items and skipped_items.

Example
Response · 200 OK
{
  "id": "evl_01j9...",
  "status": "SUCCEEDED",
  "items_total": 24,
  "items_scored": 22,
  "items_skipped": 2,
  "total_items": 24,
  "scored_items": 22,
  "skipped_items": 2,
  "avg_score": 0.87,
  "pass_rate": 0.92,
  "started_at": "2026-09-27T10:01:00Z",
  "finished_at": "2026-09-27T10:03:12Z"
}
POST/ai/evaluations/{id}/runai:manage

Enqueue the evaluation run. The run must be in PENDING status. The worker replays every dataset item through the real Seller pipeline in EVAL mode (the playground's dry run with the candidate_config: nothing is sent, drafted or stored for a customer; each replay is one run at /ai/ops/runs/{execution_id}), and the judge scores the reply against the reference — with the JUDGE prompt resolved for the business (the platform default in internal/prompts/defaults/judge.md unless the organization or business has its own active version), or JEV when the organization has it configured. One result is written per item. An item whose replay couldn't run or finish (the AI budget, the platform's switches, an unknown prompt version, a pinned model that failed, no customer message) is written as SKIPPED with its reason in error_code, isn't scored and doesn't count in pass_rate; when none could be scored, the evaluation ends FAILED with the reason in error. Returns 503 when the task queue is unavailable.

Example

Response · 202 Accepted, no body

GET/ai/evaluations/{id}/resultsai:read

One page of evaluation results, oldest-first: one row per dataset item. status is SCORED or SKIPPED. judge_scores is a JSON map of dimension name to {score, source, explanation}. The reply is unsealed in the response. A SKIPPED row has error_code (no_customer_message, budget, blocked, invalid_candidate, replay_failed or no_business) and error instead of scores.

Query: limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "evr_01j9...",
      "item_id": "edi_01j9...",
      "status": "SCORED",
      "overall_score": 0.92,
      "pass": true,
      "judge_scores": {
        "tone": { "score": 0.95, "source": "JEV" },
        "correctness": { "score": 0.89, "source": "JEV" }
      },
      "reply": "Yetkazib berish 2–3 ish kunida, 500 so'm."
    },
    {
      "id": "evr_01j9q...",
      "item_id": "edi_01j9q...",
      "status": "SKIPPED",
      "error_code": "no_customer_message",
      "error": "The item's conversation has no customer message to answer.",
      "overall_score": null,
      "pass": null,
      "judge_scores": {}
    }
  ],
  "total": 24,
  "limit": 50,
  "offset": 0
}
POST/ai/experimentsai:manage

Create an A/B experiment: dataset_id and name required, optional business_id and description. Status starts DRAFT.

Example
Request body
{
  "name": "Seller v2 vs v3",
  "dataset_id": "eds_01j9...",
  "description": "Compare tone profiles."
}
Response · 201 Created
{
  "id": "exp_01j9...",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Seller v2 vs v3",
  "status": "DRAFT",
  "winner_variant_id": null
}
GET/ai/experimentsai:read

List experiments, most-recent first.

Query: limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "exp_01j9...",
      "name": "Seller v2 vs v3",
      "status": "SUCCEEDED",
      "winner_variant_id": "exv_01j9..."
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/ai/experiments/{id}ai:read

One experiment with its variants and their linked evaluations.

Example
Response · 200 OK
{
  "id": "exp_01j9...",
  "status": "SUCCEEDED",
  "winner_variant_id": "exv_01j9...",
  "variants": [
    {
      "id": "exv_01j9...",
      "name": "Variant A",
      "evaluation_id": "evl_01j9..."
    }
  ]
}
POST/ai/experiments/{id}/variantsai:manage

Add a variant to an experiment: name required, optional candidate_config (the keys and checks of POST /ai/evaluations).

Example
Request body
{
  "name": "Variant A",
  "candidate_config": { "prompt_version_id": "prv_01j9..." }
}
Response · 201 Created
{
  "id": "exv_01j9...",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "experiment_id": "exp_01j9...",
  "name": "Variant A"
}
POST/ai/experiments/{id}/winnerai:manage

Record the winning variant and set experiment status to SUCCEEDED. variant_id required.

Example
Request body
{
  "variant_id": "exv_01j9..."
}

Response · 204 No Content, no body

AI Seller: calendar#

Google Calendar for the AI Seller. An organization connects one Google calendar through Google's consent screen (the platform's OAuth client; minimal scopes: the calendar's events, its free/busy times, and the account's e-mail address). The tokens are encrypted at rest and never returned. When a customer asks about a meeting, the Calendar agent (its own run, with the calendar prompt) turns the conversation into one action: CREATE, UPDATE, CANCEL, FIND or CHECK_AVAILABILITY. Before anything reaches Google it's validated: a start in the future in a known time zone, at least the business's notice ahead and within its booking horizon (calendar_config), inside its opening hours (business_hours), an allowed length, and no double booking (Google's free/busy and Oqim's own bookings). Then the gate decides: the customer confirms the exact time first (unless the business sets require_customer_confirmation false, at autonomy 5); the change runs by itself only at autonomy 5 with the tool granted to the Calendar agent (PATCH /ai/agents/CALENDAR/config, tool_permissions: calendar.create_event, calendar.update_event, calendar.cancel_event are off until granted); otherwise it waits for a person here. An ambiguous request hands the conversation to a person (CALENDAR_AMBIGUITY). Each action keeps its provenance: the conversation, the customer message, the Seller turn (execution_id), the Calendar agent's run (agent_execution_id), the JEV decision, the confidence, what the agent extracted (encrypted at rest), the model, the prompt version and the event. A change reaches Google once: the event's id derives from the action. Events: ai.calendar.connected, ai.calendar.disconnected, ai.calendar.action_created, ai.calendar.action_updated; the metric ai_calendar_actions counts actions by type and status.

GET/ai/calendarai:read

The calendar page: configured (the platform set up its Google client; false means nothing can connect), enabled (platform policy lets Calendar AI run), redirect_url (register it in the Google client), the connection (ACTIVE, or NEEDS_REAUTH when Google refused the stored access; null when none), tools (the Calendar agent's granted calendar tools and the writes among them), upcoming (the next 50 confirmed meetings Oqim booked, with their titles) and pending (proposals waiting for the customer or a person, and failed changes that can be retried).

Example
Response · 200 OK
{
  "configured": true,
  "enabled": true,
  "redirect_url": "https://oqim.example.com/api/v1/ai/calendar/oauth/callback",
  "scopes": [
    "openid",
    "email",
    "https://www.googleapis.com/auth/calendar.events",
    "https://www.googleapis.com/auth/calendar.freebusy"
  ],
  "connection": {
    "id": "cal_01j9s7a1c3e5g7j9m1p3r5t7w9",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "provider": "GOOGLE",
    "status": "ACTIVE",
    "account_email": "bookings@silkroad.example",
    "calendar_id": "primary",
    "scopes": [
      "openid",
      "email",
      "https://www.googleapis.com/auth/calendar.events",
      "https://www.googleapis.com/auth/calendar.freebusy"
    ],
    "last_error": null,
    "last_used_at": "2026-09-26T09:30:00Z",
    "connected_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "disconnected_by": null,
    "disconnected_at": null,
    "created_at": "2026-09-26T08:00:00Z",
    "updated_at": "2026-09-26T09:30:00Z"
  },
  "tools": {
    "granted": [
      "calendar.check_availability",
      "calendar.find_events",
      "calendar.create_event"
    ],
    "writes": ["calendar.create_event"]
  },
  "upcoming": [
    {
      "id": "cev_01j9s7b2d4f6h8k0m2p4s6v8x0",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "connection_id": "cal_01j9s7a1c3e5g7j9m1p3r5t7w9",
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
      "provider": "GOOGLE",
      "calendar_id": "primary",
      "provider_event_id": "oq6k9f2c1q8v0m3g5h7j2t4r6s8u0a1b3c5d7e9f0g2h4j6k8m0",
      "status": "CONFIRMED",
      "starts_at": "2026-09-29T06:00:00Z",
      "ends_at": "2026-09-29T06:30:00Z",
      "time_zone": "Asia/Tashkent",
      "html_link": "https://calendar.google.com/calendar/event?eid=…",
      "created_at": "2026-09-26T09:30:00Z",
      "updated_at": "2026-09-26T09:30:00Z",
      "title": "Consultation — Aziza Karimova",
      "notes": "About the wedding package",
      "conversation_title": "Aziza Karimova",
      "business_name": "Silk Road Events"
    }
  ],
  "pending": [
    {
      "id": "cac_01j9s7c3e5g7j9m1p3r5t7w9y1",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
      "connection_id": "cal_01j9s7a1c3e5g7j9m1p3r5t7w9",
      "event_id": null,
      "type": "CREATE",
      "status": "PENDING",
      "starts_at": "2026-09-29T06:00:00Z",
      "ends_at": "2026-09-29T06:30:00Z",
      "time_zone": "Asia/Tashkent",
      "extracted": {
        "type": "CREATE",
        "start": "2026-09-29T11:00",
        "title": "Consultation",
        "notes": "About the wedding package",
        "confidence": 0.92
      },
      "validation": {
        "passed": true,
        "checks": [
          { "name": "future", "passed": true },
          { "name": "business_hours", "passed": true },
          { "name": "free", "passed": true }
        ],
        "failures": []
      },
      "result": { "confirmed_in": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w" },
      "gate": "PERSON",
      "gate_reason": "AUTONOMY",
      "confidence": 0.92,
      "customer_confirmed": true,
      "reply_to_message_id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
      "execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
      "agent_execution_id": "aix_01j9s7d4f6h8k0m2p4s6v8x0z2",
      "jev_decision_id": "jvd_01j9rs4w6y8a0c2e4g6j8m0p2r",
      "model": "gpt-5.6",
      "prompt_version_id": "prv_01j9rz2e4g6j8m0p2r4t6w8y0a",
      "decided_by": null,
      "decided_at": null,
      "executed_at": null,
      "error_code": null,
      "error": null,
      "expires_at": "2026-09-29T06:00:00Z",
      "created_at": "2026-09-26T09:30:00Z",
      "updated_at": "2026-09-26T09:30:00Z",
      "conversation_title": "Aziza Karimova",
      "business_name": "Silk Road Events"
    }
  ]
}
POST/ai/calendar/connectai:manage

Start connecting: returns the Google consent screen's url for the signed-in person to open. calendar_id is the calendar to book into: primary (the default) or another calendar's ID from its settings in Google Calendar. The sign-in state is signed, bound to the organization and the person, works once and expires after ten minutes; PKCE protects the code. Connecting again replaces the current connection (and revokes its access).

409 calendar_not_configured until the platform sets GOOGLE_OAUTH_CLIENT_ID and GOOGLE_OAUTH_CLIENT_SECRET. API keys can't connect: a person does, in the console.

Example
Request body
{
  "calendar_id": "primary"
}
Response · 200 OK
{
  "url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=…&code_challenge=…&state=…",
  "calendar_id": "primary",
  "redirect_url": "https://oqim.example.com/api/v1/ai/calendar/oauth/callback"
}
GET/ai/calendar/oauth/callbackSigned in

Where Google sends the person back (a browser navigation, with the session cookie). The state must be valid, unexpired, unused and the signed-in person's, still allowed to manage the AI; then the code is exchanged, the granted scopes checked (both calendar scopes are required), the chosen calendar tried, and the tokens stored encrypted. Answers 302 to the console's /ai/calendar with ?calendar=connected, or ?calendar_error= signed_out, forbidden, invalid_state, state_expired, state_used, wrong_account, access_denied, missing_code, exchange_failed, scopes_missing, no_refresh_token, calendar_unavailable, not_configured or internal.

Query: state, code, error

Example
Response · 302 Found
HTTP/1.1 302 Found
Location: https://oqim.example.com/ai/calendar?calendar=connected
POST/ai/calendar/disconnectai:manage

Disconnect: Google's access is revoked, the tokens are deleted, and open proposals expire. The connection stays as history (status DISCONNECTED). 409 calendar_not_connected when there's none.

Example
Response · 200 OK
{
  "id": "cal_01j9s7a1c3e5g7j9m1p3r5t7w9",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "provider": "GOOGLE",
  "status": "DISCONNECTED",
  "account_email": "bookings@silkroad.example",
  "calendar_id": "primary",
  "scopes": [
    "openid",
    "email",
    "https://www.googleapis.com/auth/calendar.events",
    "https://www.googleapis.com/auth/calendar.freebusy"
  ],
  "last_error": null,
  "last_used_at": "2026-09-26T09:30:00Z",
  "connected_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "disconnected_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "disconnected_at": "2026-09-26T09:30:00Z",
  "created_at": "2026-09-26T08:00:00Z",
  "updated_at": "2026-09-26T09:30:00Z"
}
GET/ai/calendar/actionsai:read

The Calendar agent's actions, newest first, with their provenance and what the agent extracted. Filter by status (AWAITING_CUSTOMER, PENDING, EXECUTING, COMPLETED, REJECTED, FAILED, EXPIRED), type (CREATE, UPDATE, CANCEL, FIND, CHECK_AVAILABILITY), conversation_id and business_id. validation lists the checks and their failure codes (TIME_MISSING, TIME_INVALID, TIME_ZONE_UNKNOWN, TIME_ZONE_MISMATCH, IN_THE_PAST, TOO_SOON, TOO_FAR, HOURS_NOT_SET, OUTSIDE_BUSINESS_HOURS, DURATION_TOO_SHORT, DURATION_TOO_LONG, DOUBLE_BOOKING, EVENT_NOT_FOUND, EVENT_PAST, UNCHANGED); result holds free times (alternatives on a refusal, free for CHECK_AVAILABILITY) or the bookings found. gate is AUTO, CUSTOMER, PERSON, REFUSED or READ.

Query: status, type, conversation_id, business_id, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "cac_01j9s7c3e5g7j9m1p3r5t7w9y1",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
      "connection_id": "cal_01j9s7a1c3e5g7j9m1p3r5t7w9",
      "event_id": null,
      "type": "CREATE",
      "status": "PENDING",
      "starts_at": "2026-09-29T06:00:00Z",
      "ends_at": "2026-09-29T06:30:00Z",
      "time_zone": "Asia/Tashkent",
      "extracted": {
        "type": "CREATE",
        "start": "2026-09-29T11:00",
        "title": "Consultation",
        "notes": "About the wedding package",
        "confidence": 0.92
      },
      "validation": {
        "passed": true,
        "checks": [
          { "name": "future", "passed": true },
          { "name": "business_hours", "passed": true },
          { "name": "free", "passed": true }
        ],
        "failures": []
      },
      "result": { "confirmed_in": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w" },
      "gate": "PERSON",
      "gate_reason": "AUTONOMY",
      "confidence": 0.92,
      "customer_confirmed": true,
      "reply_to_message_id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
      "execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
      "agent_execution_id": "aix_01j9s7d4f6h8k0m2p4s6v8x0z2",
      "jev_decision_id": "jvd_01j9rs4w6y8a0c2e4g6j8m0p2r",
      "model": "gpt-5.6",
      "prompt_version_id": "prv_01j9rz2e4g6j8m0p2r4t6w8y0a",
      "decided_by": null,
      "decided_at": null,
      "executed_at": null,
      "error_code": null,
      "error": null,
      "expires_at": "2026-09-29T06:00:00Z",
      "created_at": "2026-09-26T09:30:00Z",
      "updated_at": "2026-09-26T09:30:00Z",
      "conversation_title": "Aziza Karimova",
      "business_name": "Silk Road Events"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/ai/calendar/actions/{id}/confirmai:operate

Confirm a proposed booking, move or cancellation (AWAITING_CUSTOMER or PENDING), or retry a FAILED one: it's validated again against the calendar as it is now, then applied once. A person's confirmation doesn't need the customer's; tell the customer in the conversation.

409 calendar_action_not_pending (already decided, replaced or past), calendar_action_read_only (reads), calendar_not_connected, calendar_needs_reauth; 409 calendar_action_invalid with details.codes when the time can't be booked anymore (the action is REJECTED); 502 calendar_unavailable when Google refused or couldn't be reached (the action is FAILED and can be confirmed again). Audited as AI_CALENDAR_ACTION_CONFIRMED.

Example
Response · 200 OK
{
  "id": "cac_01j9s7c3e5g7j9m1p3r5t7w9y1",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
  "connection_id": "cal_01j9s7a1c3e5g7j9m1p3r5t7w9",
  "event_id": "cev_01j9s7b2d4f6h8k0m2p4s6v8x0",
  "type": "CREATE",
  "status": "COMPLETED",
  "starts_at": "2026-09-29T06:00:00Z",
  "ends_at": "2026-09-29T06:30:00Z",
  "time_zone": "Asia/Tashkent",
  "extracted": {
    "type": "CREATE",
    "start": "2026-09-29T11:00",
    "title": "Consultation",
    "notes": "About the wedding package",
    "confidence": 0.92
  },
  "validation": {
    "passed": true,
    "checks": [
      { "name": "future", "passed": true },
      { "name": "business_hours", "passed": true },
      { "name": "free", "passed": true }
    ],
    "failures": []
  },
  "result": { "confirmed_in": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w" },
  "gate": "PERSON",
  "gate_reason": "AUTONOMY",
  "confidence": 0.92,
  "customer_confirmed": true,
  "reply_to_message_id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
  "execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
  "agent_execution_id": "aix_01j9s7d4f6h8k0m2p4s6v8x0z2",
  "jev_decision_id": "jvd_01j9rs4w6y8a0c2e4g6j8m0p2r",
  "model": "gpt-5.6",
  "prompt_version_id": "prv_01j9rz2e4g6j8m0p2r4t6w8y0a",
  "decided_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "decided_at": "2026-09-26T09:30:00Z",
  "executed_at": "2026-09-26T09:30:00Z",
  "error_code": null,
  "error": null,
  "expires_at": "2026-09-29T06:00:00Z",
  "created_at": "2026-09-26T09:30:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "conversation_title": "Aziza Karimova",
  "business_name": "Silk Road Events"
}
POST/ai/calendar/actions/{id}/rejectai:operate

Set a proposed action (or a failed one) aside: REJECTED, with you as decided_by. Nothing reaches the calendar. 409 calendar_action_not_pending otherwise. Audited as AI_CALENDAR_ACTION_REJECTED.

Example
Response · 200 OK
{
  "id": "cac_01j9s7c3e5g7j9m1p3r5t7w9y1",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
  "connection_id": "cal_01j9s7a1c3e5g7j9m1p3r5t7w9",
  "event_id": null,
  "type": "CREATE",
  "status": "REJECTED",
  "starts_at": "2026-09-29T06:00:00Z",
  "ends_at": "2026-09-29T06:30:00Z",
  "time_zone": "Asia/Tashkent",
  "extracted": {
    "type": "CREATE",
    "start": "2026-09-29T11:00",
    "title": "Consultation",
    "notes": "About the wedding package",
    "confidence": 0.92
  },
  "validation": {
    "passed": true,
    "checks": [
      { "name": "future", "passed": true },
      { "name": "business_hours", "passed": true },
      { "name": "free", "passed": true }
    ],
    "failures": []
  },
  "result": { "confirmed_in": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w" },
  "gate": "PERSON",
  "gate_reason": "REJECTED_BY_PERSON",
  "confidence": 0.92,
  "customer_confirmed": true,
  "reply_to_message_id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
  "execution_id": "aix_01j9rv0a2c4e6g8j0m2p4r6t8w",
  "agent_execution_id": "aix_01j9s7d4f6h8k0m2p4s6v8x0z2",
  "jev_decision_id": "jvd_01j9rs4w6y8a0c2e4g6j8m0p2r",
  "model": "gpt-5.6",
  "prompt_version_id": "prv_01j9rz2e4g6j8m0p2r4t6w8y0a",
  "decided_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "decided_at": "2026-09-26T09:30:00Z",
  "executed_at": null,
  "error_code": null,
  "error": null,
  "expires_at": "2026-09-29T06:00:00Z",
  "created_at": "2026-09-26T09:30:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "conversation_title": "Aziza Karimova",
  "business_name": "Silk Road Events"
}

AI Seller: case analysis and insights#

Every conversation in scope (private chats of accounts with Conversation Intelligence on, never ones marked personal, whether or not the AI Seller answered them) is analysed into its business's cases across five dimensions (PURCHASE_OUTCOME, LOST_REASON, DISSATISFACTION, DEMAND, SERVICE_QUALITY), with five scores (satisfaction 1–5, sentiment 1–5, purchase_intent 0–4, urgency 0–3, trust 0–3) and five flags (probabilities; true from 0.6). JEV decides, the LLM writes: your model writes each business's case catalog once (built-in templates without a model); JEV classifies a conversation in one request: LIVE 3 minutes after the customer's last message (at most every 15 minutes), FINAL when it ends (opt-out, lead won or lost, handoff resolved) or has been quiet 12 hours, when your model answers the dimensions JEV left unclear. Without JEV (off, or no key) conversations get FINAL analyses from your model only (analysis_mode LLM); with Conversation Intelligence off for the platform or the organization, nothing is analysed (OFF). JEV gets the masked recent transcript and a short business context, never ids. Your model writes only the per-conversation explanation and the board narratives, lazily per locale, grounded in the conversation or the board's numbers, sealed at rest. Updates arrive on the event stream: ai.analysis.updated, ai.insights.narrative_ready, ai.cases.generated and ai.insights.backfill. A business's catalog also holds two topic dimensions, CONTENT_TOPIC and COMMENT_TOPIC (what its posts and videos and its audience's comments are about), which conversations are never classified on: they are what the social boards group content and comments by (see Social content).

GET/ai/businesses/{id}/casesai:read

A business's case catalog in display order (dimension, then position): every case not deleted. dimension and status filter; dimension accepts the five conversation dimensions and the two topic dimensions CONTENT_TOPIC and COMMENT_TOPIC. conversations_30d counts the conversations classified into a case whose last message is within 30 days; for a topic, posts_30d or comments_30d counts its posts or its audience's comments of the last 30 days instead. The whole catalog comes by default (limit 500).

Query: dimension, status, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "cas_01j9s7r1t3v5x7z9b1d3f5h7k9",
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "dimension": "LOST_REASON",
      "key": "TOO_EXPENSIVE",
      "name": {
        "uz": "Narxi qimmatlik qildi",
        "ru": "Слишком дорого",
        "en": "Too expensive"
      },
      "description": "Found the ticket price too high or above their budget, or wanted a group discount they didn't get.",
      "hint": {
        "uz": "Narxga nima kirishini ayting yoki kichikroq paket taklif qiling.",
        "ru": "Расскажите, что входит в цену, или предложите пакет поменьше.",
        "en": "Say what the price includes, or offer a smaller package."
      },
      "status": "ACTIVE",
      "source": "LLM",
      "position": 0,
      "conversations_30d": 64,
      "posts_30d": 0,
      "comments_30d": 0,
      "created_at": "2026-09-20T08:00:00Z",
      "updated_at": "2026-09-20T08:00:00Z"
    },
    {
      "id": "cas_01j9s7s2u4w6y8a0c2e4g6j8m0",
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "dimension": "PURCHASE_OUTCOME",
      "key": "WENT_SILENT_AFTER_PRICE",
      "name": {
        "uz": "Narxni bilib, jim qoldi",
        "ru": "Пропал после цены",
        "en": "Went silent after the price"
      },
      "description": "Stopped replying right after learning the price or the terms, without saying no.",
      "hint": null,
      "status": "ACTIVE",
      "source": "LLM",
      "position": 3,
      "conversations_30d": 51,
      "posts_30d": 0,
      "comments_30d": 0,
      "created_at": "2026-09-20T08:00:00Z",
      "updated_at": "2026-09-20T08:00:00Z"
    }
  ],
  "total": 2,
  "limit": 50,
  "offset": 0
}
POST/ai/businesses/{id}/casesai:manage

Add a case (source MANUAL), last in its dimension (a conversation dimension or a topic: CONTENT_TOPIC or COMMENT_TOPIC). name needs uz, ru and en; description is English and is JEV's criterion for the case: say which conversations belong to it and where it stops. key is UPPER_SNAKE_CASE, derived from the English name when left out, unique in the dimension; NONE_APPLY, UNCLEAR and NOT_APPLICABLE are reserved. At most 30 cases per dimension.

Audited as AI_CASE_CREATED. A key in use is 409 case_key_taken; a full dimension 409 case_limit.

Example
Request body
{
  "dimension": "DEMAND",
  "name": {
    "uz": "Chegirma soʻradi",
    "ru": "Просили скидку",
    "en": "Asked for a discount"
  },
  "description": "Asked for a lower price, a promo code or a group discount.",
  "hint": { "en": "Say which discounts exist." }
}
Response · 201 Created
{
  "id": "cas_01j9s7w6y8a0c2e4g6j8m0p2r4",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "dimension": "DEMAND",
  "key": "ASKED_FOR_A_DISCOUNT",
  "name": {
    "uz": "Chegirma soʻradi",
    "ru": "Просили скидку",
    "en": "Asked for a discount"
  },
  "description": "Asked for a lower price, a promo code or a group discount.",
  "hint": { "en": "Say which discounts exist." },
  "status": "ACTIVE",
  "source": "MANUAL",
  "position": 8,
  "conversations_30d": 0,
  "posts_30d": 0,
  "comments_30d": 0,
  "created_at": "2026-09-20T08:00:00Z",
  "updated_at": "2026-09-20T08:00:00Z"
}
POST/ai/businesses/{id}/cases/generateai:manage

Write the business's catalog in the background: 6 to 12 cases per dimension (all, or the ones in dimensions), specific to its profile, offers and funnel, with names in Uzbek, Russian and English and English criteria. dimensions names the five conversation dimensions or the two topic dimensions (CONTENT_TOPIC, COMMENT_TOPIC); a topic run also reads the business's recent captions and video titles. Left out, the conversation dimensions are written. A sync whose business has no topics asks for them (and that is how a connected account gets its own topics without an owner asking), and posts or comments judged before the topics existed are judged on them once. replace: false (the default) only adds cases whose key is missing; replace: true rewrites the generated (LLM and TEMPLATE) cases and archives those the new set leaves out; people's cases stay. Each dimension is written separately; one whose answer was unusable (after a second try) gets the built-in templates, as does every dimension without a connected model. A business's first analysis writes its catalog by itself; cases added start the analysis of its past conversations.

Audited as AI_CASES_GENERATION_STARTED and AI_CASES_GENERATED. A job already running is 409 generation_running. At most 10 an hour per business.

Example
Request body
{
  "dimensions": ["LOST_REASON", "DEMAND"],
  "replace": false
}
Response · 202 Accepted
{
  "job_id": "cgj_01j9s9v5x7z9b1d3f5h7k9m1p3"
}
GET/ai/businesses/{id}/cases/generationai:read

The business's latest catalog generation: IDLE, RUNNING, DONE or FAILED (with error).

Example
Response · 200 OK
{
  "status": "DONE",
  "finished_at": "2026-09-20T08:00:12Z"
}
PATCH/ai/cases/{id}ai:manage

Change a case's name (the languages given), description, hint (null removes it), status (ACTIVE or ARCHIVED: archived cases leave new analyses) or position. The key never changes.

Audited as AI_CASE_UPDATED.

Example
Request body
{
  "name": { "en": "Price too high" },
  "status": "ACTIVE"
}
Response · 200 OK
{
  "id": "cas_01j9s7r1t3v5x7z9b1d3f5h7k9",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "dimension": "LOST_REASON",
  "key": "TOO_EXPENSIVE",
  "name": {
    "uz": "Narxi qimmatlik qildi",
    "ru": "Слишком дорого",
    "en": "Price too high"
  },
  "description": "Found the ticket price too high or above their budget, or wanted a group discount they didn't get.",
  "hint": {
    "uz": "Narxga nima kirishini ayting yoki kichikroq paket taklif qiling.",
    "ru": "Расскажите, что входит в цену, или предложите пакет поменьше.",
    "en": "Say what the price includes, or offer a smaller package."
  },
  "status": "ACTIVE",
  "source": "LLM",
  "position": 0,
  "conversations_30d": 64,
  "posts_30d": 0,
  "comments_30d": 0,
  "created_at": "2026-09-20T08:00:00Z",
  "updated_at": "2026-09-20T08:00:00Z"
}
DELETE/ai/cases/{id}ai:manage

Remove a case from the catalog and from new analyses. Conversations classified into it keep their history (boards and drill-downs still name it).

Audited as AI_CASE_DELETED.

Example

Response · 204 No Content, no body

GET/ai/conversations/{id}/analysisai:read

A conversation's analysis: analysis_mode (JEV, LLM or OFF), phase (LIVE, or FINAL once a FINAL analysis read the customer's last message; LIVE again when they write after it; null before any), scores, flags, the classification per dimension (kind CASE, UNCLEAR or NOT_APPLICABLE; source JEV, LLM or HUMAN), up to 20 past analyses, and the explanation in locale (the ?locale, else Accept-Language, else en). explanation is null until asked for in that locale; its status is READY, PENDING, NOT_CLOSED (live conversations get none) or DISABLED (with error_code: ungrounded when the model wrote numbers the conversation doesn't hold, analysis_off, no_model, generation_failed, or the AI budget's code). Personal conversations are 404.

Query: locale

Example
Response · 200 OK
{
  "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "channel": "TELEGRAM",
  "analysis_mode": "JEV",
  "phase": "FINAL",
  "analyzed_at": "2026-09-27T12:00:00Z",
  "scores": {
    "satisfaction": 2.4,
    "sentiment": 2.1,
    "purchase_intent": 1.2,
    "urgency": 0.8,
    "trust": 1.5
  },
  "flags": {
    "unanswered_question": 0.82,
    "missed_opportunity": 0.71,
    "competitor_mentioned": 0.05,
    "unavailable_item_requested": 0.1,
    "complaint": 0.2
  },
  "cases": [
    {
      "dimension": "PURCHASE_OUTCOME",
      "kind": "CASE",
      "case_id": "cas_01j9s7s2u4w6y8a0c2e4g6j8m0",
      "case_key": "WENT_SILENT_AFTER_PRICE",
      "name": {
        "uz": "Narxni bilib, jim qoldi",
        "ru": "Пропал после цены",
        "en": "Went silent after the price"
      },
      "probability": 0.78,
      "confidence": 0.74,
      "source": "JEV"
    },
    {
      "dimension": "LOST_REASON",
      "kind": "CASE",
      "case_id": "cas_01j9s7r1t3v5x7z9b1d3f5h7k9",
      "case_key": "TOO_EXPENSIVE",
      "name": {
        "uz": "Narxi qimmatlik qildi",
        "ru": "Слишком дорого",
        "en": "Too expensive"
      },
      "probability": 0.81,
      "confidence": 0.81,
      "source": "LLM"
    },
    {
      "dimension": "DISSATISFACTION",
      "kind": "NOT_APPLICABLE",
      "case_id": null,
      "case_key": null,
      "name": null,
      "probability": 0.9,
      "confidence": 0.9,
      "source": "JEV"
    },
    {
      "dimension": "DEMAND",
      "kind": "CASE",
      "case_id": "cas_01j9s7t3v5x7z9b1d3f5h7k9m1",
      "case_key": "GROUP_TICKETS",
      "name": {
        "uz": "Guruh chiptalari",
        "ru": "Групповые билеты",
        "en": "Group tickets"
      },
      "probability": 0.88,
      "confidence": 0.88,
      "source": "JEV"
    },
    {
      "dimension": "SERVICE_QUALITY",
      "kind": "UNCLEAR",
      "case_id": null,
      "case_key": null,
      "name": null,
      "probability": 0.41,
      "confidence": 0.41,
      "source": "JEV"
    }
  ],
  "explanation": {
    "status": "READY",
    "locale": "ru",
    "what_happened": "Клиент спросил цену групповых билетов, получил цену и перестал отвечать.",
    "why": "Цена оказалась выше ожиданий, а скидку для группы клиенту не предложили.",
    "key_moments": [
      {
        "message_id": "msg_01j9s4e1g3j5m7p9s1t3v5x7z9",
        "label": "Спросил цену для группы"
      },
      {
        "message_id": "msg_01j9s4f2h4k6n8q0s2u4w6y8a0",
        "label": "Цена без предложения скидки"
      }
    ],
    "generated_at": "2026-09-27T12:05:00Z"
  },
  "history": [
    {
      "analysis_id": "can_01j9s8u4w6y8a0c2e4g6j8m0p2",
      "phase": "FINAL",
      "mode": "MIXED",
      "created_at": "2026-09-27T12:00:00Z"
    },
    {
      "analysis_id": "can_01j9s8t3v5x7z9b1d3f5h7k9m1",
      "phase": "LIVE",
      "mode": "JEV",
      "created_at": "2026-09-26T18:20:00Z"
    }
  ]
}
POST/ai/conversations/{id}/analysis/refreshai:operate

Queue a LIVE analysis of the conversation now. It needs JEV: without it 409 jev_unavailable; with Conversation Intelligence off 409 analysis_off.

At most 10 an hour per conversation and 300 per organization (429 ai_rate_limited).

Example
Response · 202 Accepted
{
  "queued": true,
  "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b"
}
POST/ai/conversations/{id}/analysis/explainai:operate

Write the explanation of a closed conversation's current analysis in a locale (uz, ru or en): what happened and why, with key moments that point at the conversation's own messages. It's written from the conversation and its classification, checked (numbers must be ones the messages or the analysis hold; else written once more, then withheld), sealed and kept per conversation, locale and analysis; asking again changes nothing. A live conversation is 409 conversation_not_closed; one never analysed 409 not_analyzed.

At most 100 an hour per organization (429 ai_rate_limited).

Example
Request body
{
  "locale": "ru"
}
Response · 202 Accepted
{
  "queued": true,
  "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
  "analysis_id": "can_01j9s8u4w6y8a0c2e4g6j8m0p2",
  "locale": "ru"
}
PUT/ai/conversations/{id}/cases/{dimension}ai:operate

Correct the conversation's classification on a dimension: case_id (a case of its business in that dimension), or case_id null with kind UNCLEAR or NOT_APPLICABLE. The correction (source HUMAN) wins over later automatic analyses, except that a FINAL analysis replaces a correction made while the conversation was LIVE.

Audited as AI_CASE_ASSIGNMENT_CORRECTED with the previous classification. A conversation without a business can only take a kind (409 no_business for a case).

Example
Request body
{
  "case_id": "cas_01j9s7r1t3v5x7z9b1d3f5h7k9"
}
Response · 200 OK
{
  "dimension": "LOST_REASON",
  "kind": "CASE",
  "case_id": "cas_01j9s7r1t3v5x7z9b1d3f5h7k9",
  "case_key": "TOO_EXPENSIVE",
  "name": {
    "uz": "Narxi qimmatlik qildi",
    "ru": "Слишком дорого",
    "en": "Too expensive"
  },
  "probability": 1,
  "confidence": 1,
  "source": "HUMAN",
  "phase": "FINAL",
  "assigned_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "updated_at": "2026-09-26T09:30:00Z"
}
GET/ai/insights/boards/{board}ai:read

An insight board: BUSINESS_HEALTH (every dimension, score and flag), SALES_OUTCOMES, LOST_REASONS, HAPPINESS, DEMAND or SERVICE_QUALITY (their dimensions with the scores and flags that explain them). The period is from–to (YYYY-MM-DD in the organization's time zone, both included; the last 30 days by default, 366 at most); the previous period is as long, right before. A conversation belongs to the period of its last message. KPIs are observed (conversations, customers_replied, purchases = leads won, avg_first_reply_seconds, handoffs, opt_outs), derived (conversion) or estimated by the AI (avg_satisfaction), each with the previous period's value and the relative change. Per dimension, total counts the conversations it applied to (a case or UNCLEAR); each case has its count, share of total, previous count, change, average confidence and daily trend; with business_id, every active case of the business is listed, even at zero. Scores come with their average, distribution and daily trend; flags with the share of analysed conversations. channel (TELEGRAM, INSTAGRAM or FACEBOOK) narrows it; without it, by_channel compares the channels (rows add up to conversations). backfill shows the analysis of past conversations while it runs; narrative is null until asked for in the locale (READY, PENDING, STALE when the data changed since, with the old text, or DISABLED).

Query: business_id, channel, from, to, locale

An unknown channel is 400 invalid_channel; an unknown board 404 board_not_found.

Example
Response · 200 OK
{
  "board": "LOST_REASONS",
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "channel": null,
  "analysis_mode": "JEV",
  "period": { "from": "2026-08-29", "to": "2026-09-27" },
  "previous_period": { "from": "2026-07-30", "to": "2026-08-28" },
  "updated_at": "2026-09-27T12:00:00Z",
  "conversations": 412,
  "analyzed": 398,
  "coverage": 0.966,
  "kpis": [
    {
      "key": "conversations",
      "value": 412,
      "previous": 380,
      "change": 0.084,
      "kind": "observed"
    },
    {
      "key": "customers_replied",
      "value": 301,
      "previous": 290,
      "change": 0.038,
      "kind": "observed"
    },
    {
      "key": "purchases",
      "value": 37,
      "previous": 31,
      "change": 0.194,
      "kind": "observed"
    },
    {
      "key": "conversion",
      "value": 0.09,
      "previous": 0.082,
      "change": 0.098,
      "kind": "derived"
    },
    {
      "key": "avg_first_reply_seconds",
      "value": 41,
      "previous": 55,
      "change": -0.255,
      "kind": "observed"
    },
    {
      "key": "handoffs",
      "value": 22,
      "previous": 25,
      "change": -0.12,
      "kind": "observed"
    },
    {
      "key": "opt_outs",
      "value": 6,
      "previous": 4,
      "change": 0.5,
      "kind": "observed"
    },
    {
      "key": "avg_satisfaction",
      "value": 3.6,
      "previous": 3.9,
      "change": -0.077,
      "kind": "estimated"
    }
  ],
  "dimensions": [
    {
      "dimension": "LOST_REASON",
      "total": 181,
      "unclear": 9,
      "not_applicable": 217,
      "items": [
        {
          "case_id": "cas_01j9s7r1t3v5x7z9b1d3f5h7k9",
          "case_key": "TOO_EXPENSIVE",
          "name": {
            "uz": "Narxi qimmatlik qildi",
            "ru": "Слишком дорого",
            "en": "Too expensive"
          },
          "count": 64,
          "share": 0.354,
          "previous_count": 41,
          "change": 0.561,
          "avg_confidence": 0.81,
          "trend": [
            { "date": "2026-09-01", "count": 3 }
          ]
        }
      ]
    }
  ],
  "scores": [
    {
      "key": "purchase_intent",
      "average": 1.9,
      "previous": 2.2,
      "distribution": [
        { "bucket": 0, "count": 51 },
        { "bucket": 1, "count": 102 },
        { "bucket": 2, "count": 131 },
        { "bucket": 3, "count": 80 },
        { "bucket": 4, "count": 34 }
      ],
      "trend": [
        { "date": "2026-09-01", "value": 2.1 }
      ]
    }
  ],
  "flags": [
    {
      "key": "competitor_mentioned",
      "count": 57,
      "share": 0.143,
      "previous": 0.09
    }
  ],
  "by_channel": [
    {
      "channel": "TELEGRAM",
      "conversations": 300,
      "analyzed": 291,
      "purchases": 25,
      "conversion": 0.083,
      "avg_satisfaction": 3.7,
      "avg_first_reply_seconds": 41,
      "previous": {
        "conversations": 280,
        "purchases": 19,
        "conversion": 0.068,
        "avg_satisfaction": 3.8
      }
    },
    {
      "channel": "INSTAGRAM",
      "conversations": 112,
      "analyzed": 107,
      "purchases": 12,
      "conversion": 0.107,
      "avg_satisfaction": 3.4,
      "avg_first_reply_seconds": 44,
      "previous": {
        "conversations": 100,
        "purchases": 12,
        "conversion": 0.12,
        "avg_satisfaction": 4.1
      }
    }
  ],
  "backfill": { "status": "RUNNING", "total": 1200, "done": 340 },
  "narrative": {
    "status": "READY",
    "locale": "ru",
    "headline": "Каждый третий клиент уходит из-за цены",
    "findings": [
      {
        "text": "«Слишком дорого» — 35% ушедших клиентов, на 56% больше, чем месяцем раньше.",
        "refs": [
          {
            "dimension": "LOST_REASON",
            "case_id": "cas_01j9s7r1t3v5x7z9b1d3f5h7k9"
          }
        ]
      }
    ],
    "actions": [
      {
        "text": "Предложите групповую скидку до того, как клиент спросит о цене."
      }
    ],
    "generated_at": "2026-09-27T12:10:00Z"
  }
}
POST/ai/insights/boards/{board}/narrativeai:operate

Write the board's narrative in a locale, for a business and a channel (or all) and a period, from the board's numbers (never from messages): a headline, findings with the cases they're about, actions. Every number in it must be in the data (a share may be written as a percentage); otherwise it's written once more, then withheld (DISABLED, error_code ungrounded). Nothing is written while the stored narrative's data is unchanged (status READY); a narrative is written at most once an hour. ai.insights.narrative_ready announces it.

Within the hour: 429 narrative_rate_limited with Retry-After. At most 60 an hour per organization (429 ai_rate_limited).

Example
Request body
{
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "channel": null,
  "from": "2026-08-29",
  "to": "2026-09-27",
  "locale": "ru"
}
Response · 202 Accepted
{
  "queued": true,
  "status": "PENDING",
  "board": "LOST_REASONS",
  "locale": "ru"
}
GET/ai/insights/boards/{board}/cases/{case_id}/conversationsai:read

The conversations behind a case's number on the board: the period's conversations classified into it, newest first, with their channel, current phase, the classification's confidence, satisfaction and how they ended for the sale (outcome_case). The case must be in one of the board's dimensions.

Query: business_id, channel, from, to, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "conversation_id": "cnv_01j9s3bh3k5m7p9s1t3v5x7z9b",
      "channel": "TELEGRAM",
      "title": "Aziza Karimova",
      "account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "last_message_at": "2026-09-26T17:58:00Z",
      "phase": "FINAL",
      "confidence": 0.81,
      "satisfaction": 2.4,
      "outcome_case": {
        "case_id": "cas_01j9s7s2u4w6y8a0c2e4g6j8m0",
        "name": {
          "uz": "Narxni bilib, jim qoldi",
          "ru": "Пропал после цены",
          "en": "Went silent after the price"
        }
      }
    }
  ],
  "total": 64,
  "limit": 50,
  "offset": 0
}
GET/ai/insights/backfillai:read

The latest analysis of past conversations for a business (or, without business_id, the organization-wide one): status IDLE, RUNNING, DONE, FAILED or CANCELLED, the conversations it found (total) and has done, failed or skipped, the mode (JEV, or LLM when the language model was allowed without JEV), what it's expected to cost and has cost. IDLE says how many conversations wait and what analysing them would cost.

Query: business_id

Example
Response · 200 OK
{
  "status": "RUNNING",
  "total": 1200,
  "done": 340,
  "failed": 2,
  "skipped": 11,
  "mode": "JEV",
  "estimated_cost_micros": 192000,
  "spent_cost_micros": 53380,
  "started_at": "2026-09-27T11:00:00Z",
  "finished_at": null
}
POST/ai/insights/backfillai:operate

Analyse past conversations: every conversation in scope (never personal ones) without an analysis that read its newest message, newest first, for a business and a channel (or all) and active since a date. Quiet ones (12 hours) get FINAL analyses, others LIVE. JEV only unless allow_llm, which also lets the language model analyse without JEV, capped by the AI budget (the job stops when the budget does). It runs at 5 conversations a second per organization, resumes by itself after a restart and never sends anything. It also starts by itself when a business gets its case catalog and when a history import finishes. ai.insights.backfill reports progress (at most every 5 seconds).

Audited as AI_BACKFILL_STARTED. One runs at a time per business (409 backfill_running); without JEV and allow_llm 409 jev_unavailable; an unknown channel 400 invalid_channel. At most 20 an hour.

Example
Request body
{
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
  "channel": "INSTAGRAM",
  "since": "2026-06-01",
  "allow_llm": false
}
Response · 202 Accepted
{
  "job_id": "bfj_01j9sa05y7a9c1e3g5j7m9p1r3"
}
POST/ai/insights/backfill/cancelai:operate

Stop the running analysis of past conversations for a business (business_id in the body), or the organization-wide one. What it analysed stays.

Audited as AI_BACKFILL_CANCELLED. Nothing running is 409 backfill_not_running.

Example
Request body
{
  "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c"
}
Response · 200 OK
{
  "status": "CANCELLED",
  "total": 1200,
  "done": 340,
  "failed": 2,
  "skipped": 11,
  "mode": "JEV",
  "estimated_cost_micros": 192000,
  "spent_cost_micros": 53380,
  "started_at": "2026-09-27T11:00:00Z",
  "finished_at": "2026-09-27T11:05:00Z"
}

Social content#

What a business publishes on Instagram, Facebook and YouTube, and how people react to it. Oqim reads each account's posts and videos of the last 90 days with their metrics, their comments (newest first, down to the ones it has) and the account's daily metrics: every 6 hours for Instagram and Facebook, every 24 hours for YouTube (its quota is shared by the whole platform), and on request. Instagram and Facebook accounts come from connecting them (Channels); YouTube channels are added by their link and read with the platform's key, and a channel its owner connects (POST /channels/youtube/connect) also gives Oqim its private Analytics: watch time, average view duration, subscribers gained and lost, traffic sources, and the audience's age, gender and country. JEV judges each comment (intent, sentiment, whether it needs a reply, shows a wish to buy, names a competitor or is abusive, and which of the business's comment topics it is about) and each caption (a clear call to action, an offer or price, the strength of its opening, and which of the business's content topics it is about), in one request each, masked and without ids; without JEV the organization's AI model classifies intents and captions, in batches of 50 within a daily cap taken from its AI budget. A choice below 0.5 confidence is UNCLEAR; a flag counts when its probability is at least 0.6. The business's topics are written by the AI model from its profile, offers and recent captions (POST /ai/businesses/{id}/cases/generate with dimensions CONTENT_TOPIC and COMMENT_TOPIC), with a built-in fallback; a sync asks for them the first time it needs them, and posts or comments judged before the topics existed are judged on them once. The numbers are computed from the platforms' own metrics; the narratives are written by the AI model from the board's numbers only, and one that states a number the board doesn't have is withheld. Captions and comments are sealed at rest.

POST/channels/youtube/accountsaccounts:manage

Add a public YouTube channel by its link (youtube.com/@name, /channel/UC…, /user/…, /c/…, or a video link), its @handle or its channel id. The channel is resolved through the YouTube Data API (1 unit of the platform's quota; 2 for a video link) and stored with status PUBLIC and capability CONTENT; its first sync is queued. A channel the organization disconnected comes back.

422 channel_not_found when there's no such channel or the text isn't a YouTube link, handle or id; 422 validation_failed without url; 409 already_added for a channel already added; 409 provider_not_configured without YOUTUBE_API_KEY; 503 youtube_quota_exhausted (with Retry-After) when the day's quota is used up. Audited as YOUTUBE_CHANNEL_ADDED. Fires channels.account.updated.

Example
Request body
{
  "url": "https://www.youtube.com/@silkroadevents"
}
Response · 201 Created
{
  "id": "cha_01j9s7a1c3e5g7j9m1p3r5t7w9",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "platform": "YOUTUBE",
  "external_id": "UCq7Rw3vZkLm9Np2Rs5Tu8Vy",
  "name": "Silk Road Events",
  "username": "@silkroadevents",
  "avatar_url": "https://yt3.ggpht.com/ytc/silkroad=s240-c-k",
  "status": "PUBLIC",
  "status_reason": null,
  "status_changed_at": "2026-09-26T09:30:00Z",
  "capabilities": ["CONTENT"],
  "scopes": [],
  "token_expires_at": null,
  "business_id": null,
  "ai_enabled": false,
  "settings": {},
  "connected_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "last_sync_at": null,
  "created_at": "2026-09-26T09:30:00Z",
  "updated_at": "2026-09-26T09:30:00Z"
}
POST/channels/youtube/connectaccounts:manage

The Google consent URL to send the person to, so a YouTube channel's owner lets Oqim read that channel's private Analytics: watch time, average view duration, subscribers gained and lost, where viewers come from, and their age, gender and country. Scopes: youtube.readonly and yt-analytics.readonly. The state is signed, lasts 10 minutes and works once. A person signed in to the console connects accounts (API keys get 403 person_required); 409 provider_not_configured when the platform has no Google OAuth client.

The channel's public numbers still sync with the platform's YouTube API key without this; connecting adds the owner's own private numbers and the INSIGHTS capability. The callback is where Google returns.

Example
Response · 200 OK
{
  "url": "https://accounts.google.com/o/oauth2/v2/auth?client_id=8192…apps.googleusercontent.com&redirect_uri=https%3A%2F%2Fapp.example.com%2Fapi%2Fv1%2Fchannels%2Fyoutube%2Fcallback&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fyoutube.readonly%20https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fyt-analytics.readonly&access_type=offline&state=…"
}
GET/channels/youtube/callbackPublic

Where Google returns the browser (public; the state is verified and used once, and the browser must carry the console session of the person who started the flow, so nobody can be tricked into connecting their channel to another organization). The code is exchanged, the channel is read with the token, and the account goes from PUBLIC to CONNECTED with capabilities CONTENT and INSIGHTS; its access and refresh tokens are sealed on the account and refreshed by themselves, and its first sync is queued. Redirects (302) to PUBLIC_APP_URL/channels?connected=<id>, or ?error= denied, state_invalid, state_expired, connected_elsewhere, not_configured or provider_error.

Query: code, state, error

Example
Response · 302 Found
HTTP/1.1 302 Found
Location: https://app.example.com/channels?connected=cha_01j9s7a1c3e5g7j9m1p3r5t7w9
GET/ai/social/accounts/{id}/syncai:read

An account's content sync: status IDLE, RUNNING, DONE, FAILED or QUOTA_EXHAUSTED (the platform's YouTube quota for today is used up; it continues after the reset), with what the latest (or running) sync did: posts read, new comments stored, posts and comments analysed. error_code on FAILED: account_auth_required (the token stopped working: the account is now AUTH_REQUIRED and needs connecting again), rate_limited, missing_permissions, not_found, youtube_key_invalid or provider_error. analysis_mode is JEV, LLM or OFF; analysis_error says why some content waits unanalysed (JEV refused the key, the daily AI cap was reached, …). quota_used counts YouTube Data API units; analytics_requests counts the run's YouTube Analytics reports (its own quota), and analytics_error_code with analytics_error says why a report failed (the watch time and audience numbers are missing then, while the public numbers stay).

Example
Response · 200 OK
{
  "account_id": "cha_01j9s7b2d4f6h8k0m2p4s6v8x0",
  "platform": "INSTAGRAM",
  "status": "DONE",
  "last_sync_at": "2026-09-26T09:30:00Z",
  "posts": 42,
  "comments": 318,
  "analyzed": 327,
  "error": null,
  "error_code": null,
  "analysis_mode": "JEV",
  "analysis_error": null,
  "quota_used": 0,
  "analytics_requests": 0,
  "analytics_error_code": null,
  "analytics_error": null,
  "trigger": "SCHEDULE",
  "requested_at": null,
  "started_at": "2026-09-26T09:24:02Z",
  "finished_at": "2026-09-26T09:29:57Z",
  "next_sync_at": "2026-09-26T15:29:57Z"
}
POST/ai/social/accounts/{id}/syncai:operate

Sync an account now. A sync already running isn't started twice; the response is the sync state.

409 account_not_syncable for an account that is disconnected, waiting for a reconnect or without access to its content; 409 provider_not_configured for YouTube without a key; 429 ai_rate_limited after 6 requests an hour for one account. Audited as SOCIAL_SYNC_REQUESTED. ai.social.synced fires when it finishes.

Example
Response · 202 Accepted
{
  "account_id": "cha_01j9s7b2d4f6h8k0m2p4s6v8x0",
  "platform": "INSTAGRAM",
  "status": "RUNNING",
  "last_sync_at": "2026-09-26T09:30:00Z",
  "posts": 42,
  "comments": 318,
  "analyzed": 327,
  "error": null,
  "error_code": null,
  "analysis_mode": "JEV",
  "analysis_error": null,
  "quota_used": 0,
  "analytics_requests": 0,
  "analytics_error_code": null,
  "analytics_error": null,
  "trigger": "MANUAL",
  "requested_at": "2026-09-26T09:30:00Z",
  "started_at": "2026-09-26T09:24:02Z",
  "finished_at": null,
  "next_sync_at": "2026-09-26T15:29:57Z"
}
GET/ai/social/boards/{board}ai:read

A board: SOCIAL_OVERVIEW (every section), CONTENT (formats, top posts, caption signals, posting, trends), AUDIENCE (comment intents, flags, sentiment, trends) or GROWTH (trends and the accounts' daily metrics as account_metrics). platform (INSTAGRAM, FACEBOOK or YOUTUBE), account_id and business_id narrow it; from and to are dates in the organization's time zone (default: the 30 days ending today) and the previous period is as long, right before. KPIs: followers, follower_growth, posts, views, engagement_rate, comments, unanswered_comments (a comment that needs a reply and got none within 24 hours), purchase_intent_comments, each observed, estimated (AI judgements) or derived; CONTENT and GROWTH also report watch_time_minutes and avg_view_duration_seconds (observed) when the channel's owner connected it, otherwise null. content_topics (engagement by the business's own content topics: posts, views, the mean engagement rate and the change against the previous period, with a daily trend), comment_topics (what the comments are about: count, share, the change and a daily trend) and traffic_sources (where viewers found the videos, from the owner's Analytics) are their own sections; NONE_APPLY and UNCLEAR appear as their own rows with a null case_id. engagement_rate is (likes + comments + shares + saves) / views, or / reach where the platform has no views; for a set of posts, the sums' ratio; avg_ rates are means of posts' rates. by_platform (only without platform) has one row per platform, in a fixed order, adding up to the KPIs. best_slots use ISO weekdays (1 Monday) and hours in the organization's time zone. narrative is null until requested for the locale (?locale=, else Accept-Language, else en): READY, PENDING, STALE (the data changed since; the old text stays) or DISABLED with error_code.

Query: platform, account_id, business_id, from, to, locale

404 board_not_found for another board; 400 invalid_platform, invalid_period (from after to, or over 366 days) or invalid_locale. Disconnected accounts are left out unless asked for by account_id.

Example
Response · 200 OK
{
  "board": "SOCIAL_OVERVIEW",
  "platform": null,
  "account_id": null,
  "business_id": null,
  "period": { "from": "2026-08-28", "to": "2026-09-26" },
  "previous_period": { "from": "2026-07-29", "to": "2026-08-27" },
  "updated_at": "2026-09-26T09:30:00Z",
  "accounts": [
    {
      "id": "cha_01j9s7b2d4f6h8k0m2p4s6v8x0",
      "platform": "INSTAGRAM",
      "name": "Silk Road Events",
      "username": "silkroad.events",
      "status": "CONNECTED",
      "last_sync_at": "2026-09-26T09:30:00Z"
    },
    {
      "id": "cha_01j9s7a1c3e5g7j9m1p3r5t7w9",
      "platform": "YOUTUBE",
      "name": "Silk Road Events",
      "username": "@silkroadevents",
      "status": "PUBLIC",
      "last_sync_at": "2026-09-26T09:30:00Z"
    }
  ],
  "coverage": {
    "posts": 51,
    "posts_analyzed": 51,
    "comments": 3400,
    "comments_analyzed": 3310
  },
  "kpis": [
    {
      "key": "followers",
      "value": 12400,
      "previous": 11900,
      "change": 0.042,
      "kind": "observed"
    },
    {
      "key": "follower_growth",
      "value": 500,
      "previous": 380,
      "change": 0.316,
      "kind": "derived"
    },
    {
      "key": "posts",
      "value": 51,
      "previous": 44,
      "change": 0.159,
      "kind": "observed"
    },
    {
      "key": "views",
      "value": 412000,
      "previous": 330000,
      "change": 0.248,
      "kind": "observed"
    },
    {
      "key": "engagement_rate",
      "value": 0.0584,
      "previous": 0.0512,
      "change": 0.141,
      "kind": "derived"
    },
    {
      "key": "comments",
      "value": 3400,
      "previous": 2900,
      "change": 0.172,
      "kind": "observed"
    },
    {
      "key": "unanswered_comments",
      "value": 38,
      "previous": 61,
      "change": -0.377,
      "kind": "estimated"
    },
    {
      "key": "purchase_intent_comments",
      "value": 402,
      "previous": 297,
      "change": 0.354,
      "kind": "estimated"
    }
  ],
  "by_platform": [
    {
      "platform": "INSTAGRAM",
      "followers": 9100,
      "posts": 42,
      "views": 380000,
      "engagement_rate": 0.061,
      "comments": 2100,
      "previous": {
        "followers": 8800,
        "posts": 35,
        "views": 300000,
        "engagement_rate": 0.052,
        "comments": 1700
      }
    },
    {
      "platform": "YOUTUBE",
      "followers": 3300,
      "posts": 9,
      "views": 32000,
      "engagement_rate": 0.031,
      "comments": 1300,
      "previous": {
        "followers": 3100,
        "posts": 9,
        "views": 30000,
        "engagement_rate": 0.04,
        "comments": 1200
      }
    }
  ],
  "formats": [
    {
      "format": "REEL",
      "posts": 30,
      "avg_views": 5400,
      "avg_engagement_rate": 0.061,
      "previous_posts": 24,
      "previous_avg_engagement_rate": 0.048
    }
  ],
  "top_posts": [
    {
      "post_id": "spo_01j9s7c3e5g7j9m1p3r5t7w9y1",
      "account_id": "cha_01j9s7b2d4f6h8k0m2p4s6v8x0",
      "platform": "INSTAGRAM",
      "format": "REEL",
      "permalink": "https://www.instagram.com/reel/DAx2kLmN9pQ/",
      "caption_excerpt": "Silk Road Expo 14–16-oktyabr! Chiptalar 150 000 soʻmdan — Direct’ga yozing 👇",
      "published_at": "2026-09-19T14:05:00Z",
      "views": 21000,
      "reach": 15800,
      "likes": 1300,
      "comments": 96,
      "shares": 40,
      "saves": 120,
      "engagement_rate": 0.0743
    }
  ],
  "comment_intents": [
    {
      "key": "PRICE_QUESTION",
      "count": 410,
      "share": 0.124,
      "previous_count": 300,
      "change": 0.367,
      "trend": [
        { "date": "2026-08-28", "count": 12 }
      ]
    }
  ],
  "comment_flags": [
    {
      "key": "needs_reply",
      "count": 380,
      "share": 0.115,
      "previous": 0.08
    }
  ],
  "sentiment": {
    "average": 3.9,
    "previous": 4.1,
    "distribution": [
      { "bucket": 1, "count": 12 }
    ],
    "trend": [
      { "date": "2026-08-28", "value": 4 }
    ]
  },
  "caption_signals": [
    {
      "key": "has_clear_cta",
      "share_of_posts": 0.4,
      "engagement_with": 0.07,
      "engagement_without": 0.04
    }
  ],
  "posting": {
    "per_week": 11.9,
    "previous_per_week": 10.27,
    "best_slots": [
      {
        "weekday": 3,
        "hour": 19,
        "avg_engagement_rate": 0.08,
        "posts": 4
      }
    ]
  },
  "trends": [
    {
      "date": "2026-08-28",
      "followers": 12100,
      "views": 14000,
      "engagement_rate": 0.058,
      "posts": 1,
      "comments": 110
    }
  ],
  "content_topics": [
    {
      "case_id": "cas_01j9s7u4w6y8a0c2e4g6j8k0m2n",
      "case_key": "PRODUCT_SHOWCASE",
      "name": {
        "uz": "Mahsulot taqdimoti",
        "ru": "Показ продукта",
        "en": "Product showcase"
      },
      "posts": 22,
      "views": 240000,
      "avg_engagement_rate": 0.062,
      "previous_posts": 18,
      "previous_avg_engagement_rate": 0.051,
      "change": 0.222,
      "trend": [
        { "date": "2026-08-28", "count": 2 }
      ]
    }
  ],
  "comment_topics": [
    {
      "case_id": "cas_01j9s7v5x7z9b1d3f5h7k9m1p3r",
      "case_key": "COMMENT_PRICE",
      "name": { "uz": "Narx haqida", "ru": "О цене", "en": "About the price" },
      "count": 410,
      "share": 0.124,
      "previous_count": 300,
      "change": 0.367,
      "trend": [
        { "date": "2026-08-28", "count": 12 }
      ]
    }
  ],
  "traffic_sources": [
    {
      "key": "YT_SEARCH",
      "views": 18000,
      "share": 0.562,
      "previous_views": 14000,
      "change": 0.286
    }
  ],
  "narrative": {
    "status": "READY",
    "locale": "ru",
    "headline": "Вовлечённость выросла благодаря Reels с призывом к действию.",
    "findings": [
      {
        "text": "Reels дают вовлечённость 6,1% против 4,8% в прошлом периоде.",
        "refs": [
          { "section": "formats", "key": "REEL" }
        ]
      },
      {
        "text": "Чаще всего спрашивают о цене: 410 комментариев, 12% всех.",
        "refs": [
          { "section": "comment_intents", "key": "PRICE_QUESTION" }
        ]
      }
    ],
    "actions": [
      {
        "text": "Ответьте на 38 комментариев, которые ждут ответа больше суток."
      }
    ],
    "generated_at": "2026-09-26T09:30:00Z"
  }
}
POST/ai/social/boards/{board}/narrativeai:operate

Write (or refresh) a board's narrative in a locale: why engagement is rising or falling, what people ask in the comments, what content works. Written by the organization's AI model from the board's numbers only; every number in it must be one of the board's (rounded at most), else it's written once more and then withheld (DISABLED, error_code ungrounded). Uzbek is written in Latin letters with ʻ and ʼ, Russian with «вы». The same board, filters, period and locale are written at most once an hour unless the data changed.

422 validation_failed without locale; 429 ai_rate_limited after 30 requests an hour per organization. Fires ai.social.narrative_ready when it's ready or withheld.

Example
Request body
{
  "platform": "INSTAGRAM",
  "from": "2026-08-28",
  "to": "2026-09-26",
  "locale": "ru"
}
Response · 202 Accepted
{
  "status": "PENDING",
  "locale": "ru"
}
GET/ai/social/postsai:read

Posts and videos, newest first, or by engagement or views. from and to are dates in the organization's time zone. engagement_rate as on the boards; analysis holds the caption's signals once judged.

Query: platform, account_id, business_id, from, to, format, sort, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "spo_01j9s7c3e5g7j9m1p3r5t7w9y1",
      "account_id": "cha_01j9s7b2d4f6h8k0m2p4s6v8x0",
      "platform": "INSTAGRAM",
      "format": "REEL",
      "permalink": "https://www.instagram.com/reel/DAx2kLmN9pQ/",
      "caption_excerpt": "Silk Road Expo 14–16-oktyabr! Chiptalar 150 000 soʻmdan — Direct’ga yozing 👇",
      "published_at": "2026-09-19T14:05:00Z",
      "views": 21000,
      "reach": 15800,
      "likes": 1300,
      "comments": 96,
      "shares": 40,
      "saves": 120,
      "watch_time_seconds": 68400,
      "engagement_rate": 0.0743,
      "analysis": {
        "has_clear_cta": 0.94,
        "mentions_offer_or_price": 0.97,
        "hook_strength": 2.6,
        "engine": "JEV",
        "analyzed_at": "2026-09-26T09:30:00Z"
      },
      "metrics_updated_at": "2026-09-26T09:30:00Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/ai/social/posts/{id}ai:read

One post: its full caption (or title and description), its analysis with probabilities, its metrics at each day's sync, and its comments by intent, flag and sentiment, with how many wait for an answer and how many replies the business wrote.

Example
Response · 200 OK
{
  "id": "spo_01j9s7c3e5g7j9m1p3r5t7w9y1",
  "account_id": "cha_01j9s7b2d4f6h8k0m2p4s6v8x0",
  "platform": "INSTAGRAM",
  "format": "REEL",
  "permalink": "https://www.instagram.com/reel/DAx2kLmN9pQ/",
  "caption_excerpt": "Silk Road Expo 14–16-oktyabr! Chiptalar 150 000 soʻmdan — Direct’ga yozing 👇",
  "published_at": "2026-09-19T14:05:00Z",
  "views": 21000,
  "reach": 15800,
  "likes": 1300,
  "comments": 96,
  "shares": 40,
  "saves": 120,
  "watch_time_seconds": 68400,
  "engagement_rate": 0.0743,
  "analysis": {
    "has_clear_cta": 0.94,
    "mentions_offer_or_price": 0.97,
    "hook_strength": 2.6,
    "engine": "JEV",
    "analyzed_at": "2026-09-26T09:30:00Z"
  },
  "metrics_updated_at": "2026-09-26T09:30:00Z",
  "caption": "Silk Road Expo 14–16-oktyabr! Chiptalar 150 000 soʻmdan — Direct’ga yozing 👇",
  "title": "",
  "description": "",
  "analysis_detail": {
    "engine": "JEV",
    "model": "jev-1.13.0",
    "has_clear_cta": 0.94,
    "mentions_offer_or_price": 0.97,
    "hook_strength": { "value": 2.6, "level": "strong", "confidence": 0.71 },
    "thresholds": { "choice": 0.5, "flag": 0.6 }
  },
  "metrics_history": [
    {
      "date": "2026-09-26",
      "views": 21000,
      "reach": 15800,
      "likes": 1300,
      "comments": 96,
      "shares": 40,
      "saves": 120,
      "watch_time_seconds": 68400
    }
  ],
  "comment_breakdown": {
    "total": 96,
    "analyzed": 96,
    "unanswered": 4,
    "owner_replies": 31,
    "intents": [
      { "key": "PRICE_QUESTION", "count": 38, "share": 0.396 }
    ],
    "flags": [
      { "key": "needs_reply", "count": 44, "share": 0.458 }
    ],
    "sentiment": {
      "average": 3.8,
      "distribution": [
        { "bucket": 4, "count": 40 }
      ]
    }
  }
}
GET/ai/social/commentsai:read

The audience's comments, newest first (the business's own count as its replies instead). intent filters by an intent key or UNCLEAR; flag by needs_reply, purchase_signal, competitor_mentioned or toxic (probability at least 0.6), or unanswered. flags are the probabilities; permalink points at the comment on YouTube, at its post elsewhere.

Query: post_id, account_id, platform, intent, flag, from, to, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "sco_01j9s7d4f6h8k0m2p4s6v8x0z2",
      "post_id": "spo_01j9s7c3e5g7j9m1p3r5t7w9y1",
      "account_id": "cha_01j9s7b2d4f6h8k0m2p4s6v8x0",
      "platform": "INSTAGRAM",
      "parent_id": null,
      "author_name": "@dilnoza.t",
      "text": "Narxi qancha? Toshkentga yetkazib berasizmi?",
      "published_at": "2026-09-19T15:12:44Z",
      "like_count": 2,
      "intent": "PRICE_QUESTION",
      "intent_confidence": 0.91,
      "sentiment": 3.4,
      "flags": {
        "needs_reply": 0.93,
        "purchase_signal": 0.88,
        "competitor_mentioned": 0.02,
        "toxic": 0.01
      },
      "engine": "JEV",
      "owner_replied": false,
      "owner_replied_at": null,
      "permalink": "https://www.instagram.com/reel/DAx2kLmN9pQ/"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}

Audit log and notifications#

The organization's append-only audit trail and its in-console notifications.

GET/audit-logsaudit:read

Audit entries, newest first. from and to take a time or a date; a plain date as to includes that whole day.

Query: action, actor_id, target_id, from, to, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": 88412,
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "actor_type": "USER",
      "actor_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "actor_label": "dilnoza@silkroad.example",
      "action": "CAMPAIGN_STARTED",
      "target_type": "campaign",
      "target_id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
      "ip": "203.0.113.24",
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6)",
      "metadata": { "name": "Expo reminder, day before", "recipients": 1840 },
      "created_at": "2026-09-29T14:02:11Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/organization/support-sessionsaudit:read

Who has looked at this organization as Oqim support, and why: every session, newest first, with the administrator, the reason they gave, when it began and ended, and how many requests it made. An organization can always see this.

Query: limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "sps_01m3v8k2q5r7t9w1y3b5d7f9h1",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "organization_name": "Silk Road Events",
      "admin_user_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "admin_email": "support@oqim.example.com",
      "admin_name": "Oqim support",
      "reason": "The customer reports replies stop after the third message; checking their conversation settings.",
      "created_at": "2026-09-26T09:30:00Z",
      "expires_at": "2026-10-01T12:30:00Z",
      "ended_at": null,
      "ended_by": null,
      "entered_at": "2026-09-26T09:30:00Z",
      "status": "ACTIVE",
      "request_count": 41
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/organization/support-sessions/{id}/accessesaudit:read

Every request one support session made against this organization, oldest first, refused ones included. Another organization's session is 404, the same as one that doesn't exist.

Query: limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": 1207,
      "support_session_id": "sps_01m3v8k2q5r7t9w1y3b5d7f9h1",
      "at": "2026-09-26T09:30:00Z",
      "method": "GET",
      "status": 200,
      "path": "/api/v1/ai/conversations"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/support/enterPublic

The one-time link an administrator opens to start a support session on this hostname (a browser navigation). The ticket works once and expires after two minutes; unknown, used, expired and ended all look the same. On success it sets the session cookie, which expires with the session, and answers 302 to the console's /. Otherwise 302 to /login?support=invalid.

Query: ticket

Example
Response · 302 Found
HTTP/1.1 302 Found
Location: https://oqim.example.com/
GET/notificationsorg:read

Recent notifications and the unread count.

Example
Response · 200 OK
{
  "data": [
    {
      "id": "ntf_01j9rm2r4t6w8y0a2c4e6g8j0m",
      "kind": "account_restricted",
      "severity": "CRITICAL",
      "title": "Expo Desk was restricted by Telegram",
      "body": "Its campaigns are paused. Open @SpamBot from the account to see why.",
      "link": "/accounts",
      "read_at": null,
      "created_at": "2026-09-26T09:30:00Z"
    }
  ],
  "unread_count": 1
}
POST/notifications/readorg:read

Mark notifications read: the given ids, or all of them when ids is omitted.

Example
Request body
{
  "ids": ["ntf_01j9rm2r4t6w8y0a2c4e6g8j0m"]
}
Response · 200 OK
{
  "updated": 1,
  "unread_count": 0
}

Public#

Endpoints without authentication: the opt-out page, the console's sign-in screens and the OpenAPI document.

GET/public/opt-out/{token}Public

Describe an opt-out link: the organization, the recipient (partly masked) and whether they already opted out. An altered or incomplete token is 404 invalid_link.

Example
Response · 200 OK
{
  "organization_name": "Silk Road Events",
  "recipient": { "name": "Az***", "username": "@az***" },
  "opted_out": false
}
POST/public/opt-out/{token}Public

Confirm the opt-out. Takes effect immediately for every campaign of that organization; repeating it is harmless.

Example
Response · 200 OK
{
  "organization_name": "Silk Road Events",
  "recipient": { "name": "Az***", "username": "@az***" },
  "opted_out": true
}
GET/public/inbox/attachments/{attachment_id}Public (signed link)

Meta fetches a reply's file itself, so Oqim hands it out on a signed, short-lived URL (exp and sig query parameters, no session and no credentials in the URL). The signature covers the organization, the file and the expiry, so it opens that one file only. Streams with HTTP Range; an invalid, expired or tampered link answers 403 link_expired.

Query: exp, sig

Example
Response · 200 OK
{
  "status": "the file's bytes"
}
GET/public/configPublic

Deployment settings the console needs before sign-in, including every webhook event type and whether the platform is in maintenance.

Example
Response · 200 OK
{
  "signups_enabled": true,
  "maintenance_mode": false,
  "policy_version": "2026-09",
  "require_opt_out_link": false,
  "allow_unknown_consent": false,
  "telegram_driver": "mtproto",
  "event_types": [
    "campaign.scheduled",
    "campaign.started",
    "campaign.paused",
    "campaign.resumed",
    "campaign.completed",
    "campaign.cancelled",
    "campaign.repeat_stopped",
    "message.delivered",
    "message.failed",
    "account.connected",
    "account.restricted",
    "account.auth_required",
    "account.disconnected",
    "recipient.opted_out"
  ],
  "version": "0.1.0"
}
GET/openapi.jsonPublic

Redirect (302) to the OpenAPI 3.1 document for this API, which the docs serve at /docs/openapi.json. It's generated from this reference: point client generators, Postman or an HTTP client at it. Not rate limited.

Example
Response · 302 Found
HTTP/1.1 302 Found
Location: https://app.example.com/docs/openapi.json

Administration API#

Platform-wide endpoints under /api/v1/admin, used by the admin console. They need a console session of a platform administrator with two-factor authentication on and verified in that session; API keys are refused. Every change is written to the audit log with the actor type PLATFORM_ADMIN.

GET/admin/overviewPlatform admin

Platform KPIs, 14 days of delivery outcomes (UTC days), the five newest open abuse reports and worker health counts. kpis.workers counts workers that are online; system_errors counts failed tasks today plus failed send jobs in the last 24 hours; unreachable_proxies counts proxies whose last health check couldn't reach them.

Example
Response · 200 OK
{
  "kpis": {
    "organizations": 42,
    "active_users": 118,
    "telegram_accounts": 97,
    "running_campaigns": 6,
    "queue_depth": 1480,
    "workers": 2,
    "restricted_accounts": 1,
    "system_errors": 0,
    "unreachable_proxies": 3
  },
  "series": [
    {
      "date": "2026-09-25",
      "delivered": 18204,
      "failed": 131,
      "unavailable": 402
    }
  ],
  "recent_abuse_reports": [
    {
      "id": "abr_01j9rn5s7v9x1z3b5d7f9h1k3m",
      "organization_id": "org_01j9s0a2c4e6g8j0m2p4r6t8w0",
      "campaign_id": "cmp_01j9s1b3d5f7h9k1m3p5r7t9v1",
      "account_id": null,
      "kind": "HIGH_FAILURE_RATE",
      "severity": "MEDIUM",
      "status": "OPEN",
      "summary": "Campaign \"Autumn sale\" paused: 41% of 120 recent sends failed",
      "details": {
        "delivered": 71,
        "failed": 9,
        "unavailable": 40,
        "attempts": 120,
        "failure_rate": 0.408,
        "threshold": 0.35,
        "min_sample": 40,
        "since_previous_pause": false
      },
      "reported_by": "SYSTEM",
      "resolved_by": null,
      "resolution_note": null,
      "created_at": "2026-09-26T08:02:00Z",
      "updated_at": "2026-09-26T08:02:00Z",
      "resolved_at": null,
      "organization_name": "Bright Deals Ltd"
    }
  ],
  "workers": { "total": 3, "online": 2, "degraded": 0, "offline": 1 }
}
GET/admin/organizationsPlatform admin

All organizations, newest first, with their size, active campaigns and 30-day volume. q matches the name, slug or ID.

Query: q, status, plan, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "name": "Silk Road Events",
      "slug": "silk-road-events",
      "status": "ACTIVE",
      "plan": "PRO",
      "timezone": "Asia/Tashkent",
      "suspended_reason": null,
      "suspended_at": null,
      "aup_version": "2026-09",
      "aup_accepted_at": "2026-09-02T08:12:44Z",
      "aup_accepted_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "conversation_intelligence_enabled": true,
      "created_at": "2026-09-01T10:00:00Z",
      "updated_at": "2026-09-02T08:12:44Z",
      "accounts": 3,
      "members": 4,
      "campaigns": 18,
      "active_campaigns": 3,
      "messages_30d": 21904,
      "api_requests_30d": 4120
    }
  ],
  "total": 42,
  "limit": 50,
  "offset": 0
}
POST/admin/organizationsPlatform admin

Create an organization on a plan (FREE by default) with an existing user as its owner. An unknown owner_email is a 422: they sign up first.

Example
Request body
{
  "name": "Samarkand Tourism Board",
  "plan": "BUSINESS",
  "timezone": "Asia/Samarkand",
  "owner_email": "owner@samarkand-tourism.example"
}
Response · 201 Created
{
  "id": "org_01j9s8j0m2p4r6t8w0y2a4c6e8",
  "name": "Samarkand Tourism Board",
  "slug": "samarkand-tourism-board",
  "status": "ACTIVE",
  "plan": "BUSINESS",
  "timezone": "Asia/Samarkand",
  "suspended_reason": null,
  "suspended_at": null,
  "aup_version": null,
  "aup_accepted_at": null,
  "aup_accepted_by": null,
  "conversation_intelligence_enabled": true,
  "created_at": "2026-09-01T10:00:00Z",
  "updated_at": "2026-09-02T08:12:44Z"
}
GET/admin/organizations/{id}Platform admin

One organization: usage against its plan limits, members, account and campaign counts by status, and its latest 20 audit entries.

Example
Response · 200 OK
{
  "organization": {
    "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "name": "Silk Road Events",
    "slug": "silk-road-events",
    "status": "ACTIVE",
    "plan": "PRO",
    "timezone": "Asia/Tashkent",
    "suspended_reason": null,
    "suspended_at": null,
    "aup_version": "2026-09",
    "aup_accepted_at": "2026-09-02T08:12:44Z",
    "aup_accepted_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "conversation_intelligence_enabled": true,
    "created_at": "2026-09-01T10:00:00Z",
    "updated_at": "2026-09-02T08:12:44Z"
  },
  "usage": {
    "accounts": 3,
    "active_campaigns": 1,
    "recipients": 4210,
    "members": 4,
    "webhooks": 1
  },
  "limits": {
    "plan": "PRO",
    "accounts": 10,
    "active_campaigns": 10,
    "recipients": 50000,
    "team_members": 5,
    "webhooks": 5,
    "api_access": true,
    "analytics": "basic",
    "price_monthly_usd": 49
  },
  "members": [
    {
      "id": "mem_01j9rk9p1s3v5x7z9b1d3f5h7k",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "user_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "role": "OWNER",
      "created_at": "2026-09-01T10:00:00Z",
      "user_name": "Dilnoza Yusupova",
      "user_email": "dilnoza@silkroad.example",
      "last_login_at": "2026-09-26T07:58:10Z",
      "mfa_enabled": true
    }
  ],
  "accounts": { "ACTIVE": 3 },
  "campaigns": { "RUNNING": 1, "SCHEDULED": 2, "COMPLETED": 14, "CANCELLED": 1 },
  "recent_audit": [
    {
      "id": 88412,
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "actor_type": "USER",
      "actor_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "actor_label": "dilnoza@silkroad.example",
      "action": "CAMPAIGN_STARTED",
      "target_type": "campaign",
      "target_id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
      "ip": "203.0.113.24",
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6)",
      "metadata": { "name": "Expo reminder, day before", "recipients": 1840 },
      "created_at": "2026-09-29T14:02:11Z"
    }
  ]
}
PATCH/admin/organizations/{id}Platform admin

Rename an organization, or change its plan or time zone. A new plan's limits apply at once.

Example
Request body
{
  "plan": "BUSINESS"
}
Response · 200 OK
{
  "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Silk Road Events",
  "slug": "silk-road-events",
  "status": "ACTIVE",
  "plan": "BUSINESS",
  "timezone": "Asia/Tashkent",
  "suspended_reason": null,
  "suspended_at": null,
  "aup_version": "2026-09",
  "aup_accepted_at": "2026-09-02T08:12:44Z",
  "aup_accepted_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "conversation_intelligence_enabled": true,
  "created_at": "2026-09-01T10:00:00Z",
  "updated_at": "2026-09-02T08:12:44Z"
}
POST/admin/organizations/{id}/suspendPlatform admin

Suspend an organization: its running and scheduled campaigns pause, messaging stops, members get read-only access and a notification with the reason, which is required.

Example
Request body
{
  "reason": "Messages sent to people without a recorded basis for contact (abuse report abr_01j9rn5s…)."
}
Response · 200 OK
{
  "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Silk Road Events",
  "slug": "silk-road-events",
  "status": "SUSPENDED",
  "plan": "PRO",
  "timezone": "Asia/Tashkent",
  "suspended_reason": "Messages sent to people without a recorded basis for contact (abuse report abr_01j9rn5s…).",
  "suspended_at": "2026-09-26T09:30:00Z",
  "aup_version": "2026-09",
  "aup_accepted_at": "2026-09-02T08:12:44Z",
  "aup_accepted_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "conversation_intelligence_enabled": true,
  "created_at": "2026-09-01T10:00:00Z",
  "updated_at": "2026-09-02T08:12:44Z"
}
POST/admin/organizations/{id}/reactivatePlatform admin

Lift a suspension. Campaigns paused by it stay paused until the organization resumes them.

Example
Response · 200 OK
{
  "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Silk Road Events",
  "slug": "silk-road-events",
  "status": "ACTIVE",
  "plan": "PRO",
  "timezone": "Asia/Tashkent",
  "suspended_reason": null,
  "suspended_at": null,
  "aup_version": "2026-09",
  "aup_accepted_at": "2026-09-02T08:12:44Z",
  "aup_accepted_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "conversation_intelligence_enabled": true,
  "created_at": "2026-09-01T10:00:00Z",
  "updated_at": "2026-09-02T08:12:44Z"
}
GET/admin/usersPlatform admin

Users across all organizations, with how many organizations each belongs to. q matches the email, name or ID.

Query: q, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "email": "dilnoza@silkroad.example",
      "name": "Dilnoza Yusupova",
      "is_platform_admin": false,
      "status": "ACTIVE",
      "mfa_enabled": true,
      "last_login_at": "2026-09-26T07:58:10Z",
      "created_at": "2026-09-01T10:00:00Z",
      "updated_at": "2026-09-20T12:00:00Z",
      "organizations": 1
    }
  ],
  "total": 118,
  "limit": 50,
  "offset": 0
}
POST/admin/users/{id}/disablePlatform admin

Disable a user: they can't sign in and their sessions end at once. You can't disable yourself (409 cannot_disable_self).

Example
Response · 200 OK
{
  "id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "email": "dilnoza@silkroad.example",
  "name": "Dilnoza Yusupova",
  "is_platform_admin": false,
  "status": "DISABLED",
  "mfa_enabled": true,
  "last_login_at": "2026-09-26T07:58:10Z",
  "created_at": "2026-09-01T10:00:00Z",
  "updated_at": "2026-09-20T12:00:00Z"
}
POST/admin/users/{id}/enablePlatform admin

Let a disabled user sign in again.

Example
Response · 200 OK
{
  "id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "email": "dilnoza@silkroad.example",
  "name": "Dilnoza Yusupova",
  "is_platform_admin": false,
  "status": "ACTIVE",
  "mfa_enabled": true,
  "last_login_at": "2026-09-26T07:58:10Z",
  "created_at": "2026-09-01T10:00:00Z",
  "updated_at": "2026-09-20T12:00:00Z"
}
GET/admin/accountsPlatform admin

Telegram accounts across all organizations, most recently changed first. q matches the name, username, masked phone, organization or ID.

Query: status, q, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "telegram_user_id": 5712093344,
      "display_name": "Silk Road Support",
      "username": "silkroad_support",
      "phone_masked": "+998 •• ••• 45 17",
      "status": "ACTIVE",
      "status_reason": null,
      "status_changed_at": "2026-09-03T06:41:12Z",
      "proxy_id": null,
      "daily_limit": 150,
      "min_interval_seconds": 8,
      "cooldown_until": null,
      "next_send_at": "2026-09-26T09:30:08Z",
      "sent_today": 42,
      "sent_today_date": "2026-09-26T00:00:00Z",
      "total_sent": 3180,
      "total_failed": 12,
      "consecutive_errors": 0,
      "last_seen_at": "2026-09-26T09:29:51Z",
      "last_error": null,
      "inbound_enabled": false,
      "created_at": "2026-09-03T06:40:00Z",
      "updated_at": "2026-09-26T09:30:00Z",
      "proxy_name": null,
      "active_campaigns": 1,
      "organization_name": "Silk Road Events"
    }
  ],
  "total": 97,
  "limit": 50,
  "offset": 0
}
POST/admin/accounts/{id}/disablePlatform admin

Stop an account from sending, for example one that appears compromised. Campaigns using it pause and the organization is notified; it can't resume the account itself (409 disabled_by_admin). The optional note goes to the audit log.

Example
Request body
{
  "note": "Owner reported the phone stolen on 26 Sep."
}
Response · 200 OK
{
  "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "telegram_user_id": 5712093344,
  "display_name": "Silk Road Support",
  "username": "silkroad_support",
  "phone_masked": "+998 •• ••• 45 17",
  "status": "PAUSED",
  "status_reason": "Disabled by platform administrator",
  "status_changed_at": "2026-09-03T06:41:12Z",
  "proxy_id": null,
  "daily_limit": 150,
  "min_interval_seconds": 8,
  "cooldown_until": null,
  "next_send_at": "2026-09-26T09:30:08Z",
  "sent_today": 42,
  "sent_today_date": "2026-09-26T00:00:00Z",
  "total_sent": 3180,
  "total_failed": 12,
  "consecutive_errors": 0,
  "last_seen_at": "2026-09-26T09:29:51Z",
  "last_error": null,
  "inbound_enabled": false,
  "created_at": "2026-09-03T06:40:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "proxy_name": null,
  "active_campaigns": 1
}
POST/admin/accounts/{id}/enablePlatform admin

Restore an account a platform administrator disabled, so it can send again, and notify the organization. Campaigns paused because of it stay paused until the organization resumes them. Any other account is 409 not_disabled_by_admin.

Example
Response · 200 OK
{
  "id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "telegram_user_id": 5712093344,
  "display_name": "Silk Road Support",
  "username": "silkroad_support",
  "phone_masked": "+998 •• ••• 45 17",
  "status": "ACTIVE",
  "status_reason": null,
  "status_changed_at": "2026-09-03T06:41:12Z",
  "proxy_id": null,
  "daily_limit": 150,
  "min_interval_seconds": 8,
  "cooldown_until": null,
  "next_send_at": "2026-09-26T09:30:08Z",
  "sent_today": 42,
  "sent_today_date": "2026-09-26T00:00:00Z",
  "total_sent": 3180,
  "total_failed": 12,
  "consecutive_errors": 0,
  "last_seen_at": "2026-09-26T09:29:51Z",
  "last_error": null,
  "inbound_enabled": false,
  "created_at": "2026-09-03T06:40:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "proxy_name": null,
  "active_campaigns": 1
}
GET/admin/proxiesPlatform admin

Proxies across all organizations, newest first, with the organization, how many accounts use each one, and failed health checks over the last 24 hours. q matches the name, host, organization or ID; organization_id (or org) picks one organization. Credentials are never returned.

Query: status, protocol, organization_id, q, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "prx_01j9ra2c4e6g8j0m2p4r6t8w0y",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "name": "Tashkent office",
      "protocol": "SOCKS5",
      "host": "proxy.silkroad.example",
      "port": 1080,
      "username": "oqim",
      "status": "ACTIVE",
      "latency_ms": 84,
      "last_checked_at": "2026-09-26T09:30:00Z",
      "last_error": null,
      "disabled_reason": null,
      "disabled_at": null,
      "created_at": "2026-09-03T06:20:00Z",
      "updated_at": "2026-09-26T09:30:00Z",
      "has_credentials": true,
      "assigned_account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
      "assigned_account_name": "Silk Road Support",
      "organization_name": "Silk Road Events",
      "assigned_accounts": 1,
      "failed_checks_24h": 1,
      "last_failed_at": "2026-09-26T07:40:00Z",
      "last_failure": "dial tcp 203.0.113.8:1080: i/o timeout"
    }
  ],
  "total": 57,
  "limit": 50,
  "offset": 0
}
GET/admin/proxies/{id}Platform admin

One proxy from any organization, with its latest 50 health checks, newest first.

Example
Response · 200 OK
{
  "id": "prx_01j9ra2c4e6g8j0m2p4r6t8w0y",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Tashkent office",
  "protocol": "SOCKS5",
  "host": "proxy.silkroad.example",
  "port": 1080,
  "username": "oqim",
  "status": "ACTIVE",
  "latency_ms": 84,
  "last_checked_at": "2026-09-26T09:30:00Z",
  "last_error": null,
  "disabled_reason": null,
  "disabled_at": null,
  "created_at": "2026-09-03T06:20:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "has_credentials": true,
  "assigned_account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "assigned_account_name": "Silk Road Support",
  "organization_name": "Silk Road Events",
  "assigned_accounts": 1,
  "failed_checks_24h": 1,
  "last_failed_at": "2026-09-26T07:40:00Z",
  "last_failure": "dial tcp 203.0.113.8:1080: i/o timeout",
  "recent_checks": [
    {
      "id": 5521,
      "proxy_id": "prx_01j9ra2c4e6g8j0m2p4r6t8w0y",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "status": "ACTIVE",
      "latency_ms": 84,
      "error": null,
      "checked_at": "2026-09-26T09:30:00Z"
    },
    {
      "id": 5498,
      "proxy_id": "prx_01j9ra2c4e6g8j0m2p4r6t8w0y",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "status": "UNREACHABLE",
      "latency_ms": null,
      "error": "dial tcp 203.0.113.8:1080: i/o timeout",
      "checked_at": "2026-09-26T07:40:00Z"
    }
  ]
}
POST/admin/proxies/{id}/checkPlatform admin

Queue a health check, like the organization's bulk check; the result lands on the proxy within seconds. A check already queued in the last minute counts. A disabled proxy isn't checked (409 proxy_disabled).

Example
Response · 202 Accepted
{
  "queued": 1
}
POST/admin/proxies/{id}/disablePlatform admin

Stop Oqim connecting through a proxy, for example an open proxy that's being abused. The reason is required; the organization sees it on the proxy and in a notification. An account using the proxy stops sending, as it would on its next send: it moves to ERROR (PROXY_UNAVAILABLE) and its running and scheduled campaigns pause. It never falls back to a direct connection. The organization can't check, change or reassign the proxy until you enable it.

Example
Request body
{
  "reason": "Open proxy listed on spam blocklists; reported by Telegram on 26 Sep."
}
Response · 200 OK
{
  "id": "prx_01j9ra2c4e6g8j0m2p4r6t8w0y",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Tashkent office",
  "protocol": "SOCKS5",
  "host": "proxy.silkroad.example",
  "port": 1080,
  "username": "oqim",
  "status": "DISABLED",
  "latency_ms": 84,
  "last_checked_at": "2026-09-26T09:30:00Z",
  "last_error": null,
  "disabled_reason": "Open proxy listed on spam blocklists; reported by Telegram on 26 Sep.",
  "disabled_at": "2026-09-26T09:30:00Z",
  "created_at": "2026-09-03T06:20:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "has_credentials": true,
  "assigned_account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "assigned_account_name": "Silk Road Support",
  "organization_name": "Silk Road Events",
  "assigned_accounts": 1,
  "failed_checks_24h": 1,
  "last_failed_at": "2026-09-26T07:40:00Z",
  "last_failure": "dial tcp 203.0.113.8:1080: i/o timeout"
}
POST/admin/proxies/{id}/enablePlatform admin

Enable a disabled proxy and queue a health check; it shows as not checked until the result arrives. An account stopped because of the proxy stays stopped until the organization resumes it.

Example
Response · 200 OK
{
  "id": "prx_01j9ra2c4e6g8j0m2p4r6t8w0y",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Tashkent office",
  "protocol": "SOCKS5",
  "host": "proxy.silkroad.example",
  "port": 1080,
  "username": "oqim",
  "status": "ACTIVE",
  "latency_ms": null,
  "last_checked_at": null,
  "last_error": null,
  "disabled_reason": null,
  "disabled_at": null,
  "created_at": "2026-09-03T06:20:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "has_credentials": true,
  "assigned_account_id": "acc_01j9r3a8f2k5m7p9s1t3v5x7z9",
  "assigned_account_name": "Silk Road Support",
  "organization_name": "Silk Road Events",
  "assigned_accounts": 1,
  "failed_checks_24h": 1,
  "last_failed_at": "2026-09-26T07:40:00Z",
  "last_failure": "dial tcp 203.0.113.8:1080: i/o timeout"
}
GET/admin/campaignsPlatform admin

Campaigns across all organizations. status takes several values separated by commas.

Query: q, status, organization_id, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "name": "Expo reminder, day before",
      "description": "",
      "status": "RUNNING",
      "status_reason": null,
      "message_text": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}.\n\nNo longer want these messages? {{opt_out_url}}",
      "parse_mode": "MARKDOWN",
      "disable_link_preview": true,
      "attachment_id": null,
      "audience": {
        "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
        "tags": [],
        "recipient_ids": []
      },
      "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
      "timezone": "Asia/Tashkent",
      "start_at": "2026-09-30T05:00:00Z",
      "end_at": "2026-09-30T15:00:00Z",
      "window_start": 600,
      "window_end": 1200,
      "window_days": [1, 2, 3, 4, 5],
      "recurrence": null,
      "series_id": null,
      "occurrence": null,
      "auto_launch_at": null,
      "next_run_at": null,
      "total_recipients": 1840,
      "delivered_count": 612,
      "failed_count": 4,
      "unavailable_count": 9,
      "skipped_count": 0,
      "pending_count": 1215,
      "in_flight_count": 3,
      "launched_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "launched_at": "2026-09-29T14:02:11Z",
      "started_at": "2026-09-30T05:00:02Z",
      "paused_at": null,
      "completed_at": null,
      "cancelled_at": null,
      "created_at": "2026-09-28T10:15:00Z",
      "updated_at": "2026-09-30T08:14:10Z",
      "organization_name": "Silk Road Events"
    }
  ],
  "total": 311,
  "limit": 50,
  "offset": 0
}
GET/admin/campaigns/{id}Platform admin

One campaign from any organization.

Example
Response · 200 OK
{
  "id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Expo reminder, day before",
  "description": "",
  "status": "RUNNING",
  "status_reason": null,
  "message_text": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}.\n\nNo longer want these messages? {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "disable_link_preview": true,
  "attachment_id": null,
  "audience": {
    "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
    "tags": [],
    "recipient_ids": []
  },
  "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
  "timezone": "Asia/Tashkent",
  "start_at": "2026-09-30T05:00:00Z",
  "end_at": "2026-09-30T15:00:00Z",
  "window_start": 600,
  "window_end": 1200,
  "window_days": [1, 2, 3, 4, 5],
  "recurrence": null,
  "series_id": null,
  "occurrence": null,
  "auto_launch_at": null,
  "next_run_at": null,
  "total_recipients": 1840,
  "delivered_count": 612,
  "failed_count": 4,
  "unavailable_count": 9,
  "skipped_count": 0,
  "pending_count": 1215,
  "in_flight_count": 3,
  "launched_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "launched_at": "2026-09-29T14:02:11Z",
  "started_at": "2026-09-30T05:00:02Z",
  "paused_at": null,
  "completed_at": null,
  "cancelled_at": null,
  "created_at": "2026-09-28T10:15:00Z",
  "updated_at": "2026-09-30T08:14:10Z",
  "organization_name": "Silk Road Events"
}
POST/admin/campaigns/{id}/pausePlatform admin

Pause any organization's running or scheduled campaign. The organization is notified and can't resume it (409 paused_by_admin).

Example
Response · 200 OK
{
  "id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Expo reminder, day before",
  "description": "",
  "status": "PAUSED",
  "status_reason": "Paused by platform administrator",
  "message_text": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}.\n\nNo longer want these messages? {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "disable_link_preview": true,
  "attachment_id": null,
  "audience": {
    "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
    "tags": [],
    "recipient_ids": []
  },
  "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
  "timezone": "Asia/Tashkent",
  "start_at": "2026-09-30T05:00:00Z",
  "end_at": "2026-09-30T15:00:00Z",
  "window_start": 600,
  "window_end": 1200,
  "window_days": [1, 2, 3, 4, 5],
  "recurrence": null,
  "series_id": null,
  "occurrence": null,
  "auto_launch_at": null,
  "next_run_at": null,
  "total_recipients": 1840,
  "delivered_count": 612,
  "failed_count": 4,
  "unavailable_count": 9,
  "skipped_count": 0,
  "pending_count": 1215,
  "in_flight_count": 3,
  "launched_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "launched_at": "2026-09-29T14:02:11Z",
  "started_at": "2026-09-30T05:00:02Z",
  "paused_at": "2026-09-26T09:30:00Z",
  "completed_at": null,
  "cancelled_at": null,
  "created_at": "2026-09-28T10:15:00Z",
  "updated_at": "2026-09-30T08:14:10Z",
  "organization_name": "Silk Road Events"
}
POST/admin/campaigns/{id}/resumePlatform admin

Resume a campaign a platform administrator paused: back to RUNNING if it had started, otherwise SCHEDULED. The organization is notified. A campaign paused for any other reason is 409 not_paused_by_admin; its organization resumes it.

Example
Response · 200 OK
{
  "id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Expo reminder, day before",
  "description": "",
  "status": "RUNNING",
  "status_reason": null,
  "message_text": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}.\n\nNo longer want these messages? {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "disable_link_preview": true,
  "attachment_id": null,
  "audience": {
    "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
    "tags": [],
    "recipient_ids": []
  },
  "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
  "timezone": "Asia/Tashkent",
  "start_at": "2026-09-30T05:00:00Z",
  "end_at": "2026-09-30T15:00:00Z",
  "window_start": 600,
  "window_end": 1200,
  "window_days": [1, 2, 3, 4, 5],
  "recurrence": null,
  "series_id": null,
  "occurrence": null,
  "auto_launch_at": null,
  "next_run_at": null,
  "total_recipients": 1840,
  "delivered_count": 612,
  "failed_count": 4,
  "unavailable_count": 9,
  "skipped_count": 0,
  "pending_count": 1215,
  "in_flight_count": 3,
  "launched_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "launched_at": "2026-09-29T14:02:11Z",
  "started_at": "2026-09-30T05:00:02Z",
  "paused_at": null,
  "completed_at": null,
  "cancelled_at": null,
  "created_at": "2026-09-28T10:15:00Z",
  "updated_at": "2026-09-30T08:14:10Z",
  "organization_name": "Silk Road Events"
}
POST/admin/campaigns/{id}/cancelPlatform admin

Cancel any organization's unfinished campaign. The organization is notified; a cancelled campaign can't be restarted.

Example
Response · 200 OK
{
  "id": "cmp_01j9r4c6e8g0j2m4p6r8t0w2y4",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "name": "Expo reminder, day before",
  "description": "",
  "status": "CANCELLED",
  "status_reason": null,
  "message_text": "Hi {{first_name|there}}! Tashkent Expo opens tomorrow at 10:00 in {{location}}.\n\nNo longer want these messages? {{opt_out_url}}",
  "parse_mode": "MARKDOWN",
  "disable_link_preview": true,
  "attachment_id": null,
  "audience": {
    "list_ids": ["lst_01j9r5e2g4j6m8p0r2t4w6y8a0"],
    "tags": [],
    "recipient_ids": []
  },
  "account_ids": ["acc_01j9r3a8f2k5m7p9s1t3v5x7z9"],
  "timezone": "Asia/Tashkent",
  "start_at": "2026-09-30T05:00:00Z",
  "end_at": "2026-09-30T15:00:00Z",
  "window_start": 600,
  "window_end": 1200,
  "window_days": [1, 2, 3, 4, 5],
  "recurrence": null,
  "series_id": null,
  "occurrence": null,
  "auto_launch_at": null,
  "next_run_at": null,
  "total_recipients": 1840,
  "delivered_count": 612,
  "failed_count": 4,
  "unavailable_count": 9,
  "skipped_count": 0,
  "pending_count": 0,
  "in_flight_count": 0,
  "launched_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "launched_at": "2026-09-29T14:02:11Z",
  "started_at": "2026-09-30T05:00:02Z",
  "paused_at": null,
  "completed_at": null,
  "cancelled_at": "2026-09-26T09:30:00Z",
  "created_at": "2026-09-28T10:15:00Z",
  "updated_at": "2026-09-30T08:14:10Z",
  "organization_name": "Silk Road Events"
}
GET/admin/workersPlatform admin

Worker and scheduler processes. health is ONLINE (heartbeat under 20 s old), DEGRADED (under 90 s, or failing more than 20% of 50+ tasks) or OFFLINE.

Example
Response · 200 OK
{
  "data": [
    {
      "id": "worker-worker-01-4172",
      "kind": "WORKER",
      "host": "worker-01",
      "version": "0.1.0",
      "concurrency": 20,
      "active_jobs": 6,
      "processed_total": 184220,
      "failed_total": 312,
      "started_at": "2026-09-24T02:10:00Z",
      "last_heartbeat_at": "2026-09-26T09:29:58Z",
      "meta": {
        "queues": { "messages": 6, "webhooks": 3, "maintenance": 1 },
        "driver": "mtproto"
      },
      "health": "ONLINE"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/admin/queuesPlatform admin

Every queue: counts, today's processed and failed tasks (UTC), latency_ms (age of the oldest pending task) and seven days of history. exists is false until a queue receives its first task.

Example
Response · 200 OK
{
  "data": [
    {
      "name": "messages",
      "exists": true,
      "info": {
        "size": 1480,
        "groups": 0,
        "pending": 1452,
        "active": 6,
        "scheduled": 18,
        "retry": 4,
        "archived": 0,
        "completed": 9120,
        "aggregating": 0,
        "processed": 9344,
        "failed": 62,
        "processed_total": 184220,
        "failed_total": 312,
        "latency_ms": 1840,
        "paused": false,
        "memory_usage": 2811904,
        "timestamp": "2026-09-26T09:30:00Z"
      },
      "history": [
        { "date": "2026-09-25", "processed": 18335, "failed": 131 }
      ]
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/admin/queues/{name}/pausePlatform admin

Stop workers taking tasks from a queue; tasks wait and nothing is dropped. Returns the queue. A queue that hasn't received a task yet can't be paused (409 queue_not_started).

Example
Response · 200 OK
{
  "name": "messages",
  "exists": true,
  "info": { "pending": 1452, "active": 0, "paused": true },
  "history": []
}
POST/admin/queues/{name}/resumePlatform admin

Resume a paused queue. Returns the queue.

Example
Response · 200 OK
{
  "name": "messages",
  "exists": true,
  "info": { "pending": 1452, "active": 6, "paused": false },
  "history": []
}
GET/admin/queues/{name}/tasksPlatform admin

The tasks in one state of a queue, to find the ones that are stuck. state is pending, active, scheduled, retry, archived or completed (default archived: the ones that failed every retry). Each task is described by its metadata only (type, attempts, last error, when it runs next, payload size); the payload itself is never returned, because it carries customers' message text. Pages are whole pages: offset must be a multiple of limit.

Query: state, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "b7d2f6e0-3a51-4c8e-9d04-2a8f1c6e7b19",
      "queue": "messages",
      "type": "campaign:send",
      "state": "archived",
      "retried": 5,
      "max_retry": 5,
      "last_error": "telegram: FLOOD_WAIT_600",
      "last_failed_at": "2026-09-26T09:30:00Z",
      "next_process_at": null,
      "completed_at": null,
      "deadline": null,
      "is_orphaned": false,
      "payload_bytes": 412
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/admin/queues/{name}/tasks/{task}/retryPlatform admin

Put one archived, retrying or scheduled task back in line to run now. Retrying loses nothing, so it needs no reason, but it is audited (QUEUE_TASK_RETRIED). A task in any other state is 409 task_not_retryable; one that finished or was removed since the list loaded is 404 task_not_found.

Example
Response · 200 OK
{
  "id": "b7d2f6e0-3a51-4c8e-9d04-2a8f1c6e7b19",
  "queue": "messages",
  "type": "campaign:send",
  "state": "archived",
  "retried": 5,
  "max_retry": 5,
  "last_error": "telegram: FLOOD_WAIT_600",
  "last_failed_at": "2026-09-26T09:30:00Z",
  "next_process_at": null,
  "completed_at": null,
  "deadline": null,
  "is_orphaned": false,
  "payload_bytes": 412
}
POST/admin/queues/{name}/tasks/{task}/dropPlatform admin

Delete one task. This cannot be undone, so a reason of at least 10 characters is required and is kept in the audit log beside what was dropped (QUEUE_TASK_DROPPED). A task that is running right now is 409 task_active: let the worker finish it, or pause the queue first. 204 on success.

Example
Request body
{
  "reason": "Campaign was cancelled; its sends can never be delivered."
}

Response · 204 No Content, no body

POST/admin/queues/{name}/retry-archivedPlatform admin

Re-run every archived task in a queue. Returns how many were put back in line. Audited (QUEUE_ARCHIVED_RETRIED).

Example
Response · 200 OK
{
  "count": 12
}
POST/admin/queues/{name}/drop-archivedPlatform admin

Delete every archived task in a queue, clearing its backlog of failures. Only the dead ones: nothing still waiting, scheduled or running is touched, because dropping a queued send would silently lose a customer's message. Returns how many were deleted. A reason of at least 10 characters is required and is audited (QUEUE_ARCHIVED_DROPPED).

Example
Request body
{
  "reason": "Clearing failures from the Telegram outage on 2026-09-26; all were reviewed."
}
Response · 200 OK
{
  "count": 12
}
GET/admin/systemPlatform admin

What this deployment is: the build, the database schema it is on against the one this build expects, and whether Postgres and Redis answer. schema.up_to_date is false when the database is behind the binary, which means a start that did not finish its migrations.

Example
Response · 200 OK
{
  "version": "0.1.0",
  "environment": "production",
  "telegram_driver": "mtproto",
  "maintenance_mode": false,
  "schema": {
    "expected": 27,
    "current": 27,
    "applied_at": "2026-09-26T09:30:00Z",
    "up_to_date": true
  },
  "database": { "ok": true, "latency_ms": 2 },
  "redis": { "ok": true, "latency_ms": 1 }
}
POST/admin/organizations/{id}/support-sessionsPlatform admin

Open a read-only support session on one organization, to debug a customer's problem. A reason of at least 10 characters is required and the organization can read it; minutes (default 30, between 5 and 240) is a hard limit. The organization's notification feed and audit log both record it (SUPPORT_SESSION_STARTED). The response carries enter_url, which opens the session in the customer console's own hostname: it works once, for two minutes, and is returned only here. The session acts as a read-only role, every request under it is recorded, and it cannot reach this administration API.

Example
Request body
{
  "reason": "The customer reports replies stop after the third message; checking their conversation settings.",
  "minutes": 30
}
Response · 201 Created
{
  "id": "sps_01m3v8k2q5r7t9w1y3b5d7f9h1",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "organization_name": "Silk Road Events",
  "admin_user_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "admin_email": "support@oqim.example.com",
  "admin_name": "Oqim support",
  "reason": "The customer reports replies stop after the third message; checking their conversation settings.",
  "created_at": "2026-09-26T09:30:00Z",
  "expires_at": "2026-10-01T12:30:00Z",
  "ended_at": null,
  "ended_by": null,
  "entered_at": null,
  "status": "ACTIVE",
  "request_count": 0,
  "enter_url": "https://oqim.example.com/api/v1/support/enter?ticket=…"
}
GET/admin/support-sessionsPlatform admin

Support sessions across every organization, newest first. status is ACTIVE, ENDED or EXPIRED.

Query: organization_id, status, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "sps_01m3v8k2q5r7t9w1y3b5d7f9h1",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "organization_name": "Silk Road Events",
      "admin_user_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "admin_email": "support@oqim.example.com",
      "admin_name": "Oqim support",
      "reason": "The customer reports replies stop after the third message; checking their conversation settings.",
      "created_at": "2026-09-26T09:30:00Z",
      "expires_at": "2026-10-01T12:30:00Z",
      "ended_at": null,
      "ended_by": null,
      "entered_at": "2026-09-26T09:30:00Z",
      "status": "ACTIVE",
      "request_count": 41
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/admin/support-sessions/{id}/endPlatform admin

End a session now. Any platform administrator may end any session: it is a safety valve, not a privilege of whoever opened it. Access stops at once, because the browser session it ran as is deleted with it. The organization is told (SUPPORT_SESSION_ENDED). Ending one that already ended is harmless.

Example
Response · 200 OK
{
  "id": "sps_01m3v8k2q5r7t9w1y3b5d7f9h1",
  "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
  "organization_name": "Silk Road Events",
  "admin_user_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "admin_email": "support@oqim.example.com",
  "admin_name": "Oqim support",
  "reason": "The customer reports replies stop after the third message; checking their conversation settings.",
  "created_at": "2026-09-26T09:30:00Z",
  "expires_at": "2026-10-01T12:30:00Z",
  "ended_at": "2026-09-26T09:30:00Z",
  "ended_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "entered_at": "2026-09-26T09:30:00Z",
  "status": "ENDED",
  "request_count": 41
}
GET/admin/support-sessions/{id}/accessesPlatform admin

Every request made under one session, oldest first, refused ones included. The path only, never the query string, because a query can hold what someone searched for.

Query: limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": 1207,
      "support_session_id": "sps_01m3v8k2q5r7t9w1y3b5d7f9h1",
      "at": "2026-09-26T09:30:00Z",
      "method": "GET",
      "status": 200,
      "path": "/api/v1/ai/conversations"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/admin/organizations/{id}/limitsPlatform admin

What one organization may do, where that comes from, and how much of it is used: the plan's limits, the overrides the platform owner has set on top, the effective result, and current usage. A limit of 0 means unlimited.

Example
Response · 200 OK
{
  "plan": "FREE",
  "plan_limits": {
    "plan": "FREE",
    "accounts": 1,
    "active_campaigns": 1,
    "recipients": 1000,
    "team_members": 2,
    "webhooks": 0,
    "webhooks_enabled": false,
    "api_access": true,
    "analytics": "basic",
    "price_monthly_usd": 0
  },
  "overrides": { "team_members": 5, "webhooks_enabled": true, "webhooks": 2 },
  "effective": {
    "plan": "FREE",
    "accounts": 1,
    "active_campaigns": 1,
    "recipients": 1000,
    "team_members": 5,
    "webhooks": 2,
    "webhooks_enabled": true,
    "api_access": true,
    "analytics": "basic",
    "price_monthly_usd": 0
  },
  "usage": {
    "accounts": 1,
    "active_campaigns": 0,
    "recipients": 214,
    "members": 2,
    "webhooks": 0
  }
}
PUT/admin/organizations/{id}/limitsPlatform admin

Set one organization's limits on top of its plan, for a customer who needs more (or less) than their tier without moving to another. accounts, active_campaigns, recipients, team_members and webhooks take a whole number from 0 (unlimited) to 10,000,000; webhooks_enabled and api_access take a boolean. A limit you leave out follows the plan, and the whole set is replaced, so {"overrides": {}} clears them all. A field this API doesn't know is refused rather than ignored. A reason of at least 10 characters is required and is kept in the organization's audit log (ORGANIZATION_LIMITS_UPDATED) with the previous overrides. Lowering a limit below current usage deletes nothing: it only stops the organization adding more. Changing the plan leaves overrides in place.

Example
Request body
{
  "overrides": { "team_members": 5, "webhooks_enabled": true, "webhooks": 2 },
  "reason": "Pilot customer: five seats and webhooks while they evaluate."
}
Response · 200 OK
{
  "plan": "FREE",
  "plan_limits": {
    "plan": "FREE",
    "accounts": 1,
    "active_campaigns": 1,
    "recipients": 1000,
    "team_members": 2,
    "webhooks": 0,
    "webhooks_enabled": false,
    "api_access": true,
    "analytics": "basic",
    "price_monthly_usd": 0
  },
  "overrides": { "team_members": 5, "webhooks_enabled": true, "webhooks": 2 },
  "effective": {
    "plan": "FREE",
    "accounts": 1,
    "active_campaigns": 1,
    "recipients": 1000,
    "team_members": 5,
    "webhooks": 2,
    "webhooks_enabled": true,
    "api_access": true,
    "analytics": "basic",
    "price_monthly_usd": 0
  },
  "usage": {
    "accounts": 1,
    "active_campaigns": 0,
    "recipients": 214,
    "members": 2,
    "webhooks": 0
  }
}
GET/admin/organizations/{id}/ai/budgetsPlatform admin

One organization's AI budgets (daily and monthly, each null when unset) with this period's spending, and their combined state. The same shape the organization sees at GET /ai/budgets.

Example
Response · 200 OK
{
  "state": "SOFT_ALERT",
  "daily": null,
  "monthly": {
    "id": "abg_01j9s15f7h9k1m3p5r7t9v1x3z",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "period": "MONTHLY",
    "limit_micros": 50000000,
    "alert_thresholds": [80, 100],
    "hard_stop": true,
    "updated_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "created_at": "2026-09-20T08:00:00Z",
    "updated_at": "2026-09-20T08:00:00Z",
    "limit_usd": 50,
    "spent_micros": 41200000,
    "spent_usd": 41.2,
    "ratio": 0.824,
    "state": "SOFT_ALERT",
    "period_start": "2026-09-01T00:00:00Z",
    "resets_at": "2026-10-01T00:00:00Z"
  }
}
PUT/admin/organizations/{id}/ai/budgetsPlatform admin

Set one organization's AI budgets, with the same body as the organization's own PUT /ai/budgets (daily and monthly each take {limit_usd, alert_thresholds, hard_stop}, null removes that budget, a period you leave out stays as it is) plus a reason of at least 10 characters. The change is audited against the organization as a platform administrator's, with the reason and the previous budgets (AI_BUDGET_UPDATED).

Example
Request body
{
  "monthly": {
    "limit_usd": 25,
    "alert_thresholds": [80, 100],
    "hard_stop": true
  },
  "reason": "Runaway spend from a misconfigured loop; capped while we investigate."
}
Response · 200 OK
{
  "state": "ALLOWED",
  "daily": null,
  "monthly": {
    "id": "abg_01j9s15f7h9k1m3p5r7t9v1x3z",
    "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
    "period": "MONTHLY",
    "limit_micros": 25000000,
    "alert_thresholds": [80, 100],
    "hard_stop": true,
    "updated_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
    "created_at": "2026-09-20T08:00:00Z",
    "updated_at": "2026-09-20T08:00:00Z",
    "limit_usd": 25,
    "spent_micros": 3100000,
    "spent_usd": 3.1,
    "ratio": 0.124,
    "state": "ALLOWED",
    "period_start": "2026-09-01T00:00:00Z",
    "resets_at": "2026-10-01T00:00:00Z"
  }
}
GET/admin/organizations/{id}/ai/costsPlatform admin

What one organization has spent on AI for period today, week (the last 7 UTC days) or month (the calendar month so far, the default): in total and by business, agent (the capability), provider, model and mode, plus a day-by-day series. The same report the organization sees at GET /ai/costs.

Query: period

Example
Response · 200 OK
{
  "period": "month",
  "from": "2026-09-01",
  "to": "2026-09-26",
  "currency": "USD",
  "basis": { "requests": "observed", "tokens": "observed", "cost": "derived" },
  "totals": {
    "requests": 1840,
    "failed_requests": 1,
    "input_tokens": 3496000,
    "output_tokens": 202400,
    "embed_tokens": 25760,
    "cost_micros": 6412300,
    "cost_usd": 6.4123,
    "unpriced_requests": 0,
    "estimated_requests": 0
  },
  "by_business": [
    {
      "business_id": "biz_01j9s28g0j2m4p6r8t0w2y4a6c",
      "requests": 1790,
      "failed_requests": 1,
      "input_tokens": 3401000,
      "output_tokens": 196900,
      "embed_tokens": 25060,
      "cost_micros": 6231000,
      "cost_usd": 6.231,
      "unpriced_requests": 0,
      "estimated_requests": 0
    }
  ],
  "by_agent": [
    {
      "agent": "SELLER",
      "requests": 1210,
      "failed_requests": 1,
      "input_tokens": 2299000,
      "output_tokens": 133100,
      "embed_tokens": 16940,
      "cost_micros": 5102000,
      "cost_usd": 5.102,
      "unpriced_requests": 0,
      "estimated_requests": 0
    }
  ],
  "by_provider": [
    {
      "provider_id": "aip_01j9rp8t0w2y4a6c8e0g2j4m6p",
      "provider_kind": "OPENAI",
      "provider_name": "OpenAI",
      "requests": 1840,
      "failed_requests": 1,
      "input_tokens": 3496000,
      "output_tokens": 202400,
      "embed_tokens": 25760,
      "cost_micros": 6412300,
      "cost_usd": 6.4123,
      "unpriced_requests": 0,
      "estimated_requests": 0
    }
  ],
  "by_model": [
    {
      "provider_kind": "OPENAI",
      "model": "gpt-5.6",
      "requests": 1210,
      "failed_requests": 1,
      "input_tokens": 2299000,
      "output_tokens": 133100,
      "embed_tokens": 16940,
      "cost_micros": 5102000,
      "cost_usd": 5.102,
      "unpriced_requests": 0,
      "estimated_requests": 0
    }
  ],
  "by_mode": [
    {
      "mode": "LIVE",
      "requests": 1760,
      "failed_requests": 1,
      "input_tokens": 3344000,
      "output_tokens": 193600,
      "embed_tokens": 24640,
      "cost_micros": 6201000,
      "cost_usd": 6.201,
      "unpriced_requests": 0,
      "estimated_requests": 0
    }
  ],
  "daily": [
    {
      "date": "2026-09-26",
      "requests": 84,
      "failed_requests": 1,
      "input_tokens": 159600,
      "output_tokens": 9240,
      "embed_tokens": 1176,
      "cost_micros": 301400,
      "cost_usd": 0.3014,
      "unpriced_requests": 0,
      "estimated_requests": 0
    }
  ]
}
GET/admin/abuse-reportsPlatform admin

Abuse reports, newest first. Oqim files HIGH_FAILURE_RATE reports when it pauses a campaign for failures, OPT_OUT_SPIKE when it pauses one because recipients opt out right after receiving it, and ACCOUNT_RESTRICTIONS when an organization has three or more restricted accounts (at most one a day); administrators file MANUAL ones.

Query: status, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "abr_01j9rn5s7v9x1z3b5d7f9h1k3m",
      "organization_id": "org_01j9s0a2c4e6g8j0m2p4r6t8w0",
      "campaign_id": "cmp_01j9s1b3d5f7h9k1m3p5r7t9v1",
      "account_id": null,
      "kind": "HIGH_FAILURE_RATE",
      "severity": "MEDIUM",
      "status": "OPEN",
      "summary": "Campaign \"Autumn sale\" paused: 41% of 120 recent sends failed",
      "details": {
        "delivered": 71,
        "failed": 9,
        "unavailable": 40,
        "attempts": 120,
        "failure_rate": 0.408,
        "threshold": 0.35,
        "min_sample": 40,
        "since_previous_pause": false
      },
      "reported_by": "SYSTEM",
      "resolved_by": null,
      "resolution_note": null,
      "created_at": "2026-09-26T08:02:00Z",
      "updated_at": "2026-09-26T08:02:00Z",
      "resolved_at": null,
      "organization_name": "Bright Deals Ltd"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
POST/admin/abuse-reportsPlatform admin

File a report by hand, optionally tied to one of the organization's campaigns or accounts. The summary is up to 500 characters; details takes any JSON object.

Example
Request body
{
  "organization_id": "org_01j9s0a2c4e6g8j0m2p4r6t8w0",
  "severity": "MEDIUM",
  "summary": "Recipient emailed support: three promotional messages without ever signing up.",
  "details": { "source": "support ticket 4412" }
}
Response · 201 Created
{
  "id": "abr_01j9rn5s7v9x1z3b5d7f9h1k3m",
  "organization_id": "org_01j9s0a2c4e6g8j0m2p4r6t8w0",
  "campaign_id": null,
  "account_id": null,
  "kind": "MANUAL",
  "severity": "MEDIUM",
  "status": "OPEN",
  "summary": "Recipient emailed support: three promotional messages without ever signing up.",
  "details": { "source": "support ticket 4412" },
  "reported_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "resolved_by": null,
  "resolution_note": null,
  "created_at": "2026-09-26T08:02:00Z",
  "updated_at": "2026-09-26T08:02:00Z",
  "resolved_at": null,
  "organization_name": "Bright Deals Ltd"
}
PATCH/admin/abuse-reports/{id}Platform admin

Set a report's status (OPEN, REVIEWING, RESOLVED or DISMISSED) and resolution note, and optionally suspend its organization in the same call.

Example
Request body
{
  "status": "RESOLVED",
  "resolution_note": "Confirmed a purchased contact list. Organization suspended.",
  "suspend_organization": true
}
Response · 200 OK
{
  "id": "abr_01j9rn5s7v9x1z3b5d7f9h1k3m",
  "organization_id": "org_01j9s0a2c4e6g8j0m2p4r6t8w0",
  "campaign_id": "cmp_01j9s1b3d5f7h9k1m3p5r7t9v1",
  "account_id": null,
  "kind": "HIGH_FAILURE_RATE",
  "severity": "MEDIUM",
  "status": "RESOLVED",
  "summary": "Campaign \"Autumn sale\" paused: 41% of 120 recent sends failed",
  "details": {
    "delivered": 71,
    "failed": 9,
    "unavailable": 40,
    "attempts": 120,
    "failure_rate": 0.408,
    "threshold": 0.35,
    "min_sample": 40,
    "since_previous_pause": false
  },
  "reported_by": "SYSTEM",
  "resolved_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "resolution_note": "Confirmed a purchased contact list. Organization suspended.",
  "created_at": "2026-09-26T08:02:00Z",
  "updated_at": "2026-09-26T08:02:00Z",
  "resolved_at": "2026-09-26T09:30:00Z",
  "organization_name": "Bright Deals Ltd"
}
GET/admin/audit-logsPlatform admin

Audit entries from every organization plus platform-level ones, filtered like the organization audit log.

Query: organization_id, action, actor_id, target_id, from, to, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": 88412,
      "organization_id": "org_01j9s0a2c4e6g8j0m2p4r6t8w0",
      "actor_type": "PLATFORM_ADMIN",
      "actor_id": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "actor_label": "ops@oqim.example",
      "action": "ORGANIZATION_SUSPENDED",
      "target_type": "organization",
      "target_id": "org_01j9s0a2c4e6g8j0m2p4r6t8w0",
      "ip": "203.0.113.24",
      "user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 14_6)",
      "metadata": { "reason": "Purchased contact list", "paused_campaigns": 2 },
      "created_at": "2026-09-29T14:02:11Z"
    }
  ],
  "total": 1,
  "limit": 50,
  "offset": 0
}
GET/admin/settingsPlatform admin

Platform settings.

Example
Response · 200 OK
{
  "signups_enabled": true,
  "allow_unknown_consent": false,
  "require_opt_out_link": false,
  "default_daily_limit": 150,
  "max_daily_limit": 500,
  "default_interval_seconds": 8,
  "min_interval_seconds": 4,
  "max_flood_wait_seconds": 900,
  "failure_rate_pause_threshold": 0.35,
  "failure_rate_min_sample": 40,
  "opt_out_spike_threshold": 0.05,
  "opt_out_spike_min_count": 5,
  "max_recipients_per_campaign": 50000,
  "aup_version": "2026-09",
  "maintenance_mode": false,
  "ai_allow_private_endpoints": false,
  "ai_enabled": true,
  "ai_seller_enabled": true,
  "ai_conversation_intelligence_enabled": true,
  "ai_calendar_enabled": true,
  "ai_research_enabled": true,
  "ai_jev_enabled": true,
  "jev_platform_key_enabled": true
}
PUT/admin/settingsPlatform admin

Update platform settings. Keys you leave out keep their value. failure_rate_pause_threshold and opt_out_spike_threshold are fractions between 0 and 1; a campaign pauses when at least opt_out_spike_min_count recipients opt out after receiving it and they make up opt_out_spike_threshold of its deliveries. Services pick changes up within about five seconds.

Example
Request body
{
  "require_opt_out_link": true
}
Response · 200 OK
{
  "signups_enabled": true,
  "allow_unknown_consent": false,
  "require_opt_out_link": true,
  "default_daily_limit": 150,
  "max_daily_limit": 500,
  "default_interval_seconds": 8,
  "min_interval_seconds": 4,
  "max_flood_wait_seconds": 900,
  "failure_rate_pause_threshold": 0.35,
  "failure_rate_min_sample": 40,
  "opt_out_spike_threshold": 0.05,
  "opt_out_spike_min_count": 5,
  "max_recipients_per_campaign": 50000,
  "aup_version": "2026-09",
  "maintenance_mode": false,
  "ai_allow_private_endpoints": false,
  "ai_enabled": true,
  "ai_seller_enabled": true,
  "ai_conversation_intelligence_enabled": true,
  "ai_calendar_enabled": true,
  "ai_research_enabled": true,
  "ai_jev_enabled": true,
  "jev_platform_key_enabled": true
}
GET/admin/billingPlatform admin

Plan limits, organizations per plan, and a page of organizations with their usage over 30 days.

Query: q, plan, limit, offset

Example
Response · 200 OK
{
  "plans": [
    {
      "plan": "PRO",
      "accounts": 10,
      "active_campaigns": 10,
      "recipients": 50000,
      "team_members": 5,
      "webhooks": 5,
      "api_access": true,
      "analytics": "basic",
      "price_monthly_usd": 49
    }
  ],
  "organizations": [
    {
      "id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "name": "Silk Road Events",
      "plan": "PRO",
      "status": "ACTIVE",
      "accounts": 3,
      "members": 4,
      "active_campaigns": 1,
      "messages_30d": 21904,
      "api_requests_30d": 4120
    }
  ],
  "total": 42,
  "limit": 50,
  "offset": 0,
  "totals_by_plan": { "FREE": 29, "PRO": 9, "BUSINESS": 3, "ENTERPRISE": 1 }
}
GET/admin/ai/overviewPlatform admin

AI across the platform: the AI switches, usage and cost today and this month, the tenants with the most spending over 30 days (with their failed and blocked runs of the last 24 hours), runs by agent over 24 hours, models used without a price, and provider health: attempts, failures and latency per provider over 24 hours, and the provider connections cooling off now after repeated failures.

Example
Response · 200 OK
{
  "switches": {
    "ai_enabled": true,
    "ai_seller_enabled": true,
    "ai_conversation_intelligence_enabled": true,
    "ai_calendar_enabled": true,
    "ai_research_enabled": true,
    "ai_jev_enabled": true,
    "jev_platform_key_enabled": true
  },
  "today": {
    "requests": 3120,
    "failed_requests": 1,
    "input_tokens": 5928000,
    "output_tokens": 343200,
    "embed_tokens": 43680,
    "cost_micros": 10412000,
    "cost_usd": 10.412,
    "unpriced_requests": 0,
    "estimated_requests": 0
  },
  "month": {
    "requests": 71840,
    "failed_requests": 1,
    "input_tokens": 136496000,
    "output_tokens": 7902400,
    "embed_tokens": 1005760,
    "cost_micros": 248331000,
    "cost_usd": 248.331,
    "unpriced_requests": 0,
    "estimated_requests": 0
  },
  "tenants": [
    {
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "organization_name": "Silk Road Events",
      "requests_today": 84,
      "cost_micros_today": 301400,
      "requests_30d": 1840,
      "failed_requests_30d": 3,
      "cost_micros_30d": 6412300,
      "cost_usd_30d": 6.4123,
      "unpriced_requests_30d": 0,
      "executions_24h": 91,
      "failed_executions_24h": 1,
      "blocked_executions_24h": 0
    }
  ],
  "agents_24h": [
    {
      "agent": "SELLER",
      "executions": 2210,
      "failed": 14,
      "blocked": 0
    }
  ],
  "unpriced_models": [
    {
      "provider_kind": "CUSTOM",
      "model": "qwen3:8b",
      "requests_30d": 312
    }
  ],
  "providers": [
    {
      "provider_kind": "OPENAI",
      "requests_24h": 2980,
      "failures_24h": 21,
      "error_rate": 0.007,
      "avg_latency_ms": 1840
    }
  ],
  "cooling": [
    {
      "provider_id": "aip_01j9rq1v3x5z7b9d1f3h5k7m9p",
      "model": "qwen3:8b",
      "until": "2026-09-26T09:30:30Z",
      "failures": 3,
      "last_error_code": "timeout",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "organization_name": "Silk Road Events",
      "provider_name": "Office GPU server",
      "provider_kind": "CUSTOM"
    }
  ]
}
GET/admin/ai/modelsPlatform admin

The whole model registry: platform entries first, then organization entries with their organization's name. organization_id filters (platform for platform entries only); q matches the model ID or name.

Query: organization_id, provider_kind, q, limit, offset

Example
Response · 200 OK
{
  "data": [
    {
      "id": "aim_01j9rs4w6y8a0c2e4g6j8m0p2r",
      "organization_id": null,
      "provider_kind": "OPENAI",
      "model_id": "gpt-5.6",
      "display_name": "GPT-5.6",
      "capabilities": ["CHAT", "STRUCTURED"],
      "context_window": 400000,
      "price_in_per_mtok": 1.25,
      "price_out_per_mtok": 10,
      "price_embed_per_mtok": null,
      "enabled": true,
      "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "updated_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "created_at": "2026-09-20T08:00:00Z",
      "updated_at": "2026-09-26T09:30:00Z",
      "scope": "PLATFORM",
      "priced": true
    },
    {
      "id": "aim_01j9s8fp1s3v5x7z9b1d3f5h7k",
      "organization_id": "org_01j9r2m6k4n8q3v5w7x9y1z2a3",
      "provider_kind": "OPENAI",
      "model_id": "gpt-5.6",
      "display_name": "GPT-5.6",
      "capabilities": ["CHAT", "STRUCTURED"],
      "context_window": 400000,
      "price_in_per_mtok": 1,
      "price_out_per_mtok": 8,
      "price_embed_per_mtok": null,
      "enabled": true,
      "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "updated_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
      "created_at": "2026-09-20T08:00:00Z",
      "updated_at": "2026-09-26T09:30:00Z",
      "scope": "ORGANIZATION",
      "priced": true,
      "organization_name": "Silk Road Events"
    }
  ],
  "total": 2,
  "limit": 50,
  "offset": 0
}
POST/admin/ai/modelsPlatform admin

Register a model: provider_kind and model_id as the provider lists it, capabilities (CHAT, STRUCTURED, EMBEDDINGS; default CHAT and STRUCTURED), context_window, and prices in USD per million tokens for input, output and embedding tokens (null when not known: calls are then unpriced). With organization_id the entry applies to that organization only, over the platform's. Nothing is pre-filled: enter the provider's current prices.

Example
Request body
{
  "provider_kind": "OPENAI",
  "model_id": "gpt-5.6",
  "display_name": "GPT-5.6",
  "capabilities": ["CHAT", "STRUCTURED"],
  "context_window": 400000,
  "price_in_per_mtok": 1.25,
  "price_out_per_mtok": 10
}
Response · 201 Created
{
  "id": "aim_01j9rs4w6y8a0c2e4g6j8m0p2r",
  "organization_id": null,
  "provider_kind": "OPENAI",
  "model_id": "gpt-5.6",
  "display_name": "GPT-5.6",
  "capabilities": ["CHAT", "STRUCTURED"],
  "context_window": 400000,
  "price_in_per_mtok": 1.25,
  "price_out_per_mtok": 10,
  "price_embed_per_mtok": null,
  "enabled": true,
  "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "updated_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "created_at": "2026-09-20T08:00:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "scope": "PLATFORM",
  "priced": true
}
PATCH/admin/ai/models/{id}Platform admin

Change a registry entry's name, capabilities, context window, prices or enabled (a disabled model is hidden from organizations but still prices past and running usage). Its scope, provider and model ID don't change. New prices apply to calls from then on, within a minute everywhere.

Example
Request body
{
  "price_in_per_mtok": 1.1,
  "price_out_per_mtok": 9
}
Response · 200 OK
{
  "id": "aim_01j9rs4w6y8a0c2e4g6j8m0p2r",
  "organization_id": null,
  "provider_kind": "OPENAI",
  "model_id": "gpt-5.6",
  "display_name": "GPT-5.6",
  "capabilities": ["CHAT", "STRUCTURED"],
  "context_window": 400000,
  "price_in_per_mtok": 1.1,
  "price_out_per_mtok": 9,
  "price_embed_per_mtok": null,
  "enabled": true,
  "created_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "updated_by": "usr_01j9r2n0d5c7b9a1e3f5g7h9jk",
  "created_at": "2026-09-20T08:00:00Z",
  "updated_at": "2026-09-26T09:30:00Z",
  "scope": "PLATFORM",
  "priced": true
}
PreviousAuthenticationNext SDK