Status: LIVE. The portal is obey.bitrix24.kz, the webhook is configured,
DATA_SOURCE=bitrix24, and the sync worker runs in production — .do/app.yaml
starts it as the sync service (npm run bitrix:worker). The field mapping was
confirmed against the live portal; see the header of
src/server/integrations/crm/bitrix24/mapping.ts.
This file used to open “no credentials have been supplied, and no connection to a real portal has been attempted”, and the table below still listed the mapping as not done, long after both had shipped. A status line that is wrong is worse than no status line: it is the first thing a reader believes.
| Piece | State |
|---|---|
CrmProvider interface |
Done |
| Webhook authentication | Done |
| Rate limiting (token bucket) | Done |
| Retry with jittered exponential backoff | Done |
| Request timeouts | Done |
Cursor pagination (start / next) |
Done |
| Credential redaction in errors and logs | Done |
| Sync engine: idempotent upsert, watermarks, audit log | Done |
| Deletion sweep (guarded) | Done — runs hourly, SYNC_SWEEP_EVERY |
| Field mapping | Done — confirmed against the live portal |
| Stage ID → category mapping | Done — confirmed against the live portal |
src/server/integrations/crm/bitrix24/mapping.ts shipped with every field
declared sourceField: '' and confirmed: false, and stayed that way until
somebody had read the actual portal.
Pre-filling plausible guesses like OPPORTUNITY or ASSIGNED_BY_ID would have
been worse than leaving it blank. A Bitrix24 portal is heavily customised: this
business has its own pipelines, its own stage IDs and its own custom fields. A
wrong-but-plausible mapping imports silently and produces a dashboard that looks
authoritative and is wrong. An empty mapping refuses to run.
assertMappingComplete() still enforces this at startup: enabling
DATA_SOURCE=bitrix24 with an unconfirmed mapping fails with the exact list of
unmapped fields. It is the reason the integration could only ever go live
against a portal somebody had actually looked at.
BITRIX24_WEBHOOK_URL in .env. It embeds an
access token — treat it as a password. Never commit it. The URL must be
https; the app rejects http.mapping.ts, fill in
sourceField from the real portal and set confirmed: true.BITRIX24_STAGE_CATEGORIES with the portal’s
stage IDs (C1:NEW and similar) mapped to NEW / IN_PROGRESS / WON /
LOST. This one matters most: every won/lost, revenue and conversion figure
depends on it.map* methods in Bitrix24CrmProvider. The transport
helper fetchPage is already there; only translation is needed.POST /api/v1/sync/run with mode=FULL against a staging
database, then reconcile the totals against Bitrix24’s own reports before
pointing production at it.DATA_SOURCE=bitrix24.Nothing in the frontend, the analytics engine, the database schema or the API changes at any step.
These need answers from someone who knows the Bitrix24 setup. They are not blocking today’s work, but they are blocking step 2.
PAYMENTS: false and the API returns
unavailable rather than zero.Deletions. When a deal is deleted in Bitrix24, should it disappear from the dashboard or be retained for historical accuracy?
The machinery exists: a full sync can sweep records the source no longer
reports (sweepDeleted: true), guarded so it never runs on an incremental
read, after a failed or partial run, or on an empty read. It is enabled
for the demo seed and off for Bitrix24 until this is answered.
The trade-off is real either way. Leaving it off means a deleted deal counts toward revenue forever. Turning it on means anything hidden from the webhook’s scope — a restricted pipeline, a permissions change — reads as a deletion and is removed. Answer this before the first production sync.
Bitrix24 throttles per portal, and exceeding the limit can block the portal for
its real users — not just for us. BITRIX24_RATE_LIMIT_RPS defaults to 2, which
is conservative. Raise it only with evidence.
Retries use full jitter so that several failing workers do not retry in lockstep.
redact() strips URL paths and token-shaped strings from every error before
it reaches a log.webhookUrl, token, authorization, password
and related keys..env is gitignored; .env.example carries no real values.