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 / Administration

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

Administration

For the team that runs an Oqim deployment: the platform admin console, the oqimctl command, configuration, backups, health checks, metrics and logs.

Admin console#

Platform administrators manage the whole deployment at /admin, or on an admin. host routed to it. Opening it takes a platform-admin user, a console session (API keys can't use it) and two-factor authentication: an administrator without it is asked to enroll on the first visit, and every new session asks for a code.

SectionWhat you can do
OverviewPlatform numbers, 14 days of delivered and failed messages, worker health and open abuse reports.
OrganizationsSearch tenants, create one for a customer with its owner, change plans, suspend with a reason, reactivate.
UsersFind users across organizations; disable and re-enable them.
Telegram accountsEvery connected account with its organization, status and pace; disable one, and restore it later.
ProxiesEvery organization's proxies with their health checks and the account each one serves; check one now, or disable one that's being misused.
CampaignsEvery campaign with its progress; pause or cancel one.
WorkersEach worker and scheduler process: heartbeat, version, concurrency, active jobs, throughput, uptime.
QueuesDepth, retries, latency and seven days of history per queue; pause and resume a queue, list the tasks that are stuck, and retry or drop them (dropping needs a written reason).
Support accessOpen a read-only session on one organization to debug its problem. You give a reason and a length; the organization is notified, sees who looked and why, and can read every request the session made. It cannot change anything and cannot reach this console.
Abuse reportsReview automatic and manual reports; resolve, dismiss or suspend the organization.
Audit logEvery organization's audit trail, plus platform-admin actions, with their details.
BillingPlans side by side and each organization's 30-day usage.
Limits, AI budgets and AI costPer organization: override the plan's limits for one customer without moving them to another plan, set their AI spending caps, and see what they spent by business, agent, provider and model. Every change needs a written reason and appears in the organization's own audit log.
Platform settingsSign-ups, policy version, consent rules, pacing bounds, abuse thresholds and maintenance mode, which makes the platform read-only for customers while you work.
  • Every admin action is written to the audit log with the actor type PLATFORM_ADMIN, including in the affected organization's trail.
  • Suspending an organization pauses its running and scheduled campaigns, stops its messaging and makes it read-only; its members see your reason in their console. Reactivating doesn't resume those campaigns; the organization does.
  • A campaign you pause, or an account you disable, can't be resumed by its organization. The organization is notified and told to contact support. Restore the account from the console; resume the campaign with POST /api/v1/admin/campaigns/{id}/resume.
  • Disabling a proxy needs a reason, which the organization sees. Oqim stops connecting through the proxy at once: the account using it stops sending and its campaigns pause, and it never falls back to a direct connection. The organization can give the account another proxy or a direct connection; only you can enable the proxy again.
  • New organizations need an owner who already has an Oqim sign-in.
  • Settings changes reach every service within about five seconds.

Automatic brakes#

Every 30 seconds the scheduler looks for signs that messages are reaching people who didn't ask for them. The campaign rules pause the running campaign, and every rule opens an abuse report for you to review. The campaign thresholds are under Platform settings → Abuse protection.

RuleTrips when (defaults)What happens
High failure rateFailed and unreachable sends are more than 35% of a campaign's attempts, once it has made 40.The campaign pauses and a HIGH_FAILURE_RATE report opens.
Opt-out spikeAt least 5 recipients opted out after the campaign's message reached them, and they make up 5% or more of its deliveries.The campaign pauses and an OPT_OUT_SPIKE report opens with the counts, split by where the opt-outs were recorded (opt-out link, the organization, an import).
Restricted accountsAn organization has three or more accounts Telegram restricted.An ACCOUNT_RESTRICTIONS report opens, at most one a day per organization.

The organization is notified of a pause and can resume the campaign. After an automatic pause, only what happens since then counts toward the same rule, so a resumed campaign is judged on its new sends: opt-outs from messages sent before the pause don't pause it again.

oqimctl#

oqimctl performs operator tasks and reads the same environment variables as the services. With Docker Compose, run it in the worker container, which has SESSION_KEY: docker compose exec worker oqimctl ….

CommandWhat it does
oqimctl gen-keysPrints fresh APP_KEY, SESSION_KEY and INTERNAL_TOKEN values.
oqimctl migrateApplies database migrations. The API also migrates when it starts; concurrent runs wait for each other.
oqimctl create-admin -email E -password P -name NCreates a platform administrator, or promotes an existing user and resets their password. The password needs at least 10 characters.
oqimctl seed-demoCreates the demo organization Silk Road Events with simulated accounts, recipients and campaigns (owner demo@oqim.dev). It needs SESSION_KEY. For local development only.
Shell
oqimctl gen-keys
oqimctl migrate
oqimctl create-admin -email ops@example.com -password 'a-long-passphrase' -name 'Operations'

After create-admin, sign in to the console, open /admin and set up two-factor authentication.

Configuration#

Services read their configuration from environment variables and report every missing required one together at startup.

VariableUsed byDefaultDescription
APP_ENValldevelopmentdevelopment or production. Production turns on secure cookies and HSTS by default.
HTTP_ADDRapi:8080Listen address of the REST API.
INTERNAL_ADDRworker:8090Listen address of the worker's internal Telegram gateway.
GATEWAY_URLapihttp://localhost:8090Where the API reaches the worker gateway.
INTERNAL_TOKENapi, workerrequiredShared secret that authenticates the API to the worker gateway.
DATABASE_URLallrequiredPostgres connection string.
REDIS_URLallredis://localhost:6379/0Redis for queues, rate limits and live events.
APP_KEY, APP_KEY_IDallrequired, k132-byte key (base64) that seals app secrets and signs tokens and opt-out links. The ID labels it for rotation.
SESSION_KEY, SESSION_KEY_IDworker onlyrequired, s132-byte key (base64) that seals Telegram sessions. Never give it to the API.
TELEGRAM_DRIVERworkersimulatorsimulator or mtproto for real Telegram.
TELEGRAM_APP_ID, TELEGRAM_APP_HASHworker—The operator's api_id and api_hash from my.telegram.org. Required with mtproto.
PUBLIC_APP_URLapihttp://localhost:3000The console's public URL, used in opt-out and invitation links.
ALLOWED_ORIGINSapiPUBLIC_APP_URLComma-separated origins allowed to make cookie-authenticated changes. Include the admin host.
COOKIE_SECUREapitrue in productionMark the session cookie Secure. Leave it on behind HTTPS.
COOKIE_DOMAINapihost onlySet to .example.com to share the session between the console and admin hosts.
STORAGE_DIRapi, worker./data/attachmentsWhere attachments are stored. The API and workers must share it.
WORKER_CONCURRENCYworker20Tasks one worker process runs at once.
METRICS_ADDRapi, worker, scheduler:9101, :9102, :9103Listen address for Prometheus metrics; the defaults differ per service. Keep it private.

Also available: SESSION_TTL_HOURS (console session lifetime, default 336), LOG_LEVEL=debug, and WORKER_ID to name a worker process. The web app takes API_URL at build time: where it forwards /api/v1.

Key handling

Generate keys with oqimctl gen-keys and keep them in a secret manager. Losing SESSION_KEY means every Telegram account has to sign in again; losing APP_KEY invalidates two-factor enrollments, webhook secrets and proxy credentials.

Hosts#

One web deployment serves three surfaces:

HostServes
app.example.comThe console, and the API at /api/v1
admin.example.comThe admin console (/admin)
docs.example.comThese docs (/docs)

Sign-in, invitation and opt-out pages work on every host. Locally, /admin and /docs work directly. To share one sign-in between the console and admin hosts, set COOKIE_DOMAIN to the parent domain and list both hosts in ALLOWED_ORIGINS.

infrastructure/nginx/oqim.conf is an example edge configuration: it terminates TLS for all three hosts and forwards everything to the web app, which proxies /api/v1 to the API. Whatever you put in front, allow request bodies of at least 25 MB for imports and attachments, and turn response buffering off for /api/v1/events/stream.

Backups and recovery#

On a server deployed with deploy.sh (see deploy/README.md), the database is dumped with pg_dump in its custom format into .deploy/backups/ next to the checkout:

  • ./deploy.sh backup makes a dump now, named oqim-YYYYMMDD-HHMMSS-manual.dump.
  • Every deploy makes one before it applies migrations, named oqim-YYYYMMDD-HHMMSS-before-BUILD.dump. The last 10 dumps are kept (KEEP_BACKUPS changes that), and ./deploy.sh --no-backup skips the dump.
  • The dumps stay on the server, so copy them somewhere else on a schedule. They cover Postgres only, not attachment files: on the local volume, back up oqim_attachments yourself; on S3, turn on the bucket's versioning or your provider's backups.

Keep a copy of .env.prod off the server

Proxy passwords, webhook secrets and two-factor secrets are sealed with APP_KEY, and Telegram sessions with SESSION_KEY. A dump restored without those keys can't open them, so keep a copy of .env.prod somewhere safe away from the server, and never change either key on a live server.

Restoring a dump

Stop the services that write to the database, restore with pg_restore, then start them again:

Shell
./deploy.sh dc stop api worker scheduler
./deploy.sh dc exec -T postgres pg_restore -U oqim -d oqim --clean --if-exists < .deploy/backups/oqim-YYYYMMDD-HHMMSS-….dump
./deploy.sh dc start api worker scheduler

Rolling back

./deploy.sh rollback runs the previous build again (or ./deploy.sh rollback TAG a given kept one) without rebuilding. It doesn't undo migrations: the older build runs against the newer schema, which is why migrations only ever add. To get the data back as it was before a deploy, restore the dump that deploy made.

Health checks#

EndpointReturns
GET /healthzLiveness. Always 200 with {"status":"ok"} while the process serves requests.
GET /readyzReadiness. 200 when Postgres and Redis answer, otherwise 503 with postgres_unavailable or redis_unavailable.

Both are served by the API at its root, outside /api/v1. Workers report health through their heartbeat, visible under Workers in the admin console: online within 20 seconds of the last heartbeat, degraded within 90 seconds or while erroring, offline after that.

Metrics#

The API, workers and scheduler expose Prometheus metrics on METRICS_ADDR: :9101, :9102 and :9103 by default. docker compose --profile monitoring up starts a Prometheus and a Grafana that scrape them.

MetricTypeLabelsMeaning
jobs_created_totalcounter–Send jobs created by the scheduler.
jobs_completed_totalcounterresultSend jobs finished, by result.
jobs_failed_totalcounterresultSend jobs that ended without delivery, by result.
campaigns_runninggauge–Campaigns in RUNNING.
queue_depthgaugequeueTasks waiting per queue (pending, scheduled and retry).
worker_activegauge–Tasks running in this worker.
worker_errors_totalcounter–Unexpected worker errors.
telegram_errors_totalcountercodeTelegram errors by type, e.g. FLOOD_WAIT or PEER_FLOOD.
account_restrictions_totalcounter–Accounts moved to RESTRICTED.
proxy_failures_totalcounter–Failed proxy health checks.
api_requests_totalcountermethod, route, statusHTTP requests.
api_latency_secondshistogrammethod, routeHTTP request latency.

Alerts worth having

PromQL
# Any account restricted in the last hour
increase(account_restrictions_total[1h]) > 0

# The messages queue keeps growing
deriv(queue_depth{queue="messages"}[15m]) > 0

# More than 2% of API requests failing
sum(rate(api_requests_total{status=~"5.."}[5m])) / sum(rate(api_requests_total[5m])) > 0.02

# Workers hitting unexpected errors
rate(worker_errors_total[10m]) > 0

Logs#

Every service writes structured JSON to standard output, one event per line, tagged with service and env. The API logs each request with its method, route pattern, status, duration, request ID and client IP, plus the actor and organization when authenticated. Set LOG_LEVEL=debug for more detail.

API request log line
{"time":"2026-09-26T09:30:00.412Z","level":"INFO","msg":"http","service":"api","env":"production","method":"POST","route":"/api/v1/campaigns/{id}/pause","status":200,"ms":18,"request_id":"api-01/Kx9e2QmPzR-000042","ip":"203.0.113.24","actor":"usr_01j9r2n0d5c7b9a1e3f5g7h9jk","org":"org_01j9r2m6k4n8q3v5w7x9y1z2a3"}

Errors the API hides from clients (internal_error) are logged in full with the request path, so search the logs by path and time when a user reports one. See Security for what Oqim stores.

PreviousCompliance