Documentation
WhatsApp ↔ Bitrix24 Contact Center Bridge — install, connect, and operate.
What it is
A self-hosted Laravel bridge that lands inbound WhatsApp messages in the
Bitrix24 Contact Center (one Open Channel, single operator,
auto-creating a Lead per new customer) and relays the operator's replies back to the
customer on WhatsApp — using the official Meta WhatsApp Cloud API.
Stack: Laravel · PHP 8.3+ · SQLite/MySQL · Laravel Queues · Pest.
Scope: text and media (image · document · audio · video · sticker) both directions,
one connector → one Open Channel, single operator.
Architecture
Customer WhatsApp
⇅
Meta WhatsApp Cloud API (Graph API out / webhook in)
⇅
Laravel Bridge (this app, public HTTPS)
⇅
Bitrix24 Contact Center (imconnector.send.messages in / OnImConnectorMessageAdd out)
- Inbound: Customer → Meta →
POST /webhook/whatsapp → dedupe + map → imconnector.send.messages → Open Channel → operator.
- Outbound: Operator replies in Bitrix →
OnImConnectorMessageAdd → POST /bitrix/events → Meta Graph /{phone-number-id}/messages → customer.
Features
| Feature | Detail |
| Two-way text messaging | Inbound to the Open Channel and outbound operator replies, both queue-backed. |
| Two-way media | Image, document, audio, video & sticker both directions. Files are re-hosted on the public disk so each side can fetch them (Meta media needs a token; Bitrix links need auth). Operator-reply BBCode is converted to WhatsApp formatting. |
| Auto Lead creation | New customer numbers create a Bitrix Lead automatically. |
| Idempotent delivery | Inbound deduped on wamid; outbound deduped on the Bitrix message id. |
| Self-healing retries | Jobs retry with [10,30,60]s backoff, then dead-letter to failed_jobs. |
| OAuth token management | Bitrix grant refreshed proactively (scheduler) and on demand inside the REST client. |
| Encrypted credential store | Secrets encrypted in the DB, edited via a token-gated admin form. |
| Per-request audit log | Every webhook recorded with a correlation request_id + raw payload. |
| Health probe & DR replay | /health JSON for monitors; stored payloads are replayable after a fix. |
| 24-hour window guard | Replies outside Meta's 24h customer-service window are blocked and noted in-chat. |
Requirements
- PHP 8.3+ (
pdo, mbstring, openssl, curl, bcmath) + Composer
- A database (SQLite is fine for v1; MySQL/MariaDB to scale)
- Public HTTPS (web server + TLS cert) — Meta and Bitrix must reach it
- A process supervisor for the queue worker + scheduler (systemd / Supervisor)
php artisan storage:link — media is re-hosted under {APP_URL}/storage/…, which must be publicly reachable
APP_KEY is critical. Stored credentials are encrypted with it. Set it once and never
rotate it, or every stored secret (and OAuth token) becomes unreadable. Back it up with the database.
Step-by-step setup
- Deploy & configure. Install dependencies, set
APP_URL (your public HTTPS URL) and a strong ADMIN_UI_TOKEN in .env, then migrate.
composer install
php artisan key:generate
php artisan migrate
- Enter credentials at /admin/credentials (open once with
?token=<ADMIN_UI_TOKEN>). Fill all Meta values and the Bitrix portal / client id / client secret.
- Create the Bitrix Local Application (Developer resources → Local application): type Server, handler path
{APP_URL}/bitrix/events, install path {APP_URL}/bitrix/install, scopes crm, imconnector, imopenlines, im. Copy its client id / secret into the form.
- Install the app from inside Bitrix. Bitrix POSTs the OAuth grant to
/bitrix/install automatically (you don't visit that URL yourself). You should see the "WhatsApp bridge installed" page.
- Register the connector. Makes WhatsApp (Zuse) appear in Contact Center and binds the reply event.
php artisan bitrix:register-connector
- Bind to an Open Channel. Contact Center → WhatsApp (Zuse) → Connect → bind to a new Open Channel. Save its numeric LINE id into the credentials form (Bitrix → Open Channel (LINE) ID).
- Activate the connector on that line. important Registration alone does not connect the connector to a line — without this, sends fail with
NOT_ACTIVE_LINE.
php artisan bitrix:activate-connector
# expects STATUS:true, CONFIGURED:true
- Subscribe the Meta webhook. Meta app → WhatsApp → Configuration → Webhook: URL
{APP_URL}/webhook/whatsapp, your verify token, subscribe the messages field.
- Verify the WhatsApp token, then run the workers.
php artisan whatsapp:check-token # expects "OK — token is valid."
php artisan queue:work # relays
php artisan schedule:work # token refresh + pruning
Done when: a WhatsApp to your business number appears in the Open Channel (Lead created),
and a reply from Bitrix arrives back on WhatsApp.
Credentials reference
| Group | Fields |
| Meta WhatsApp | App ID · App Secret · Access Token (permanent System User token) · Phone Number ID · WABA ID · Webhook Verify Token |
| Bitrix24 | Portal URL · Client ID · Client Secret · Connector ID · Open Channel (LINE) ID · Application Token |
Application Token verifies inbound /bitrix/events calls. Paste the
token from your Bitrix Outbound webhook (or local-app) here so events authenticate. If left blank, the
bridge bootstraps it from the first event whose member_id matches the installed grant.
Resolution order for any credential: settings table (form) → config/services.php (.env) → default. Secret fields show •••••••• once set — leave blank to keep the current value.
CLI commands
| Command | What it does |
bitrix:register-connector | Registers WhatsApp (Zuse) in Contact Center and binds OnImConnectorMessageAdd → /bitrix/events. --fresh unregisters first. |
bitrix:activate-connector | Connects the registered connector to an Open Channel line (--line=ID, defaults to bitrix.line_id) and verifies STATUS/CONFIGURED. Required before sending. |
whatsapp:check-token | Probes the Meta phone-number node to confirm the access token works (catches the 190 auth error early). |
bridge:replay-events | Reprocesses stored inbound payloads (idempotent) — DR after a fix or data loss. |
bridge:prune-events | Deletes old processed/skipped audit rows (PII retention). Runs daily 03:00. |
Routes
| Method | URI | Purpose |
| GET | / | Landing page |
| GET | /docs | This documentation |
| GET | /health | Monitoring probe (no auth, no secrets) |
| GET | /webhook/whatsapp | Meta verification handshake |
| POST | /webhook/whatsapp | Inbound messages (audit-logged + HMAC-gated) |
| POST | /bitrix/events | Operator replies (audit-logged + application_token-gated) |
| GET/POST | /bitrix/install | Bitrix OAuth install handler |
| GET/POST | /admin/credentials | Credentials form (ADMIN_UI_TOKEN-gated) |
| GET | /setup | Live readiness checklist (ADMIN_UI_TOKEN-gated) |
Reliability & observability
- Delivery guarantees: Meta retries inbound webhooks (payload persisted before we 200); relay jobs retry 5× then dead-letter. Re-deliveries are deduped both directions.
- Audit log: every
/webhook/whatsapp and /bitrix/events call writes a webhook_events row (request id, source, status, raw payload, duration).
- Health:
GET /health returns 200 healthy / 503 when the DB is unreachable, with queue depth and failed-job counts.
- Recovery:
php artisan queue:retry all after an outage; bridge:replay-events to reprocess stored payloads.
Troubleshooting
| Symptom | Likely cause / fix |
invalid_token on a Bitrix call | Stored OAuth grant isn't valid for the app/portal (never installed, reinstalled, or placeholder). Re-install the local app, then retry. |
invalid_grant / "token refresh failed" | The refresh token is fake/revoked/from another app. Re-install to mint a fresh grant. Confirm the client id/secret match the current app. |
NOT_ACTIVE_LINE | Connector registered but not connected to the line. Run php artisan bitrix:activate-connector. |
Operator replies 403 at /bitrix/events | bitrix.application_token doesn't match the token Bitrix sends. Paste the correct Outbound-webhook / local-app token into the form (or clear it to let the bridge bootstrap it). |
WhatsApp send fails 401 / code 190 | Meta access token invalid or expired. Generate a non-expiring System User token and re-save; verify with whatsapp:check-token. |
Meta error 131030 | Using a test number — add the recipient to the allowed list, or move to a production number. |
| Inbound 200 but nothing in Bitrix | Queue worker not running, or wrong bitrix.line_id / connector not activated. Check queue:failed. |
| Reply blocked, system note in chat | Outside Meta's 24-hour window (templates are v2). Message status = blocked_window. |
| Stored credentials suddenly unreadable | APP_KEY changed — restore the original key. |