Skip to Content
Living documentation — last reviewed 2026-05-28
FeaturesWhatsapp MessagingGreen API stopgap (unofficial transport)

Green API stopgap (unofficial transport)

Status: temporary. Exists only because Meta Business Verification has been pending since ~2026-08-01, blocking Access Verification → App Review → the Cloud API go-live. Delete this transport once Meta approves.

What it is

green-api.com  is an unofficial WhatsApp gateway: it runs a headless WhatsApp Web client on their servers and links it to a number as a linked device — the same slot a laptop takes. Meta never sees an API consumer, which is exactly why it needs no approval, no Business Verification, no App Review, no template review, and has no 24-hour service window.

It is also, unambiguously, a WhatsApp ToS violation. The connected number can be banned — permanently, per Green’s own docs (“inability to use the number”, from “systematic use of unofficial clients”). That risk lands on the gym, not on Taikan.

Risk controls built into the code

These are not optional polish; they are the reason the integration is survivable.

ControlWhereWhy
Instance-level send pacing, default 15s between messagesGreenApiConfigService.sendDelayMs, pushed to the instance at provision time as delaySendMessagesMillisecondsGreen’s anti-ban guidance. Their floor is 500ms; we default to the safe end. Pacing is enforced by their queue, so the worker doesn’t need a per-connection limiter.
Broadcast cap of 200 recipients/dayGREEN_API_DAILY_RECIPIENT_CAP, enforced in WhatsAppBroadcastsService.processGreen’s documented ceiling for a warmed number. Overflow is counted as skipped and logged — never silently truncated.
Per-org PostHog flag whatsapp-green-transportGreenConnectService.isConfiguredThe env vars are a global switch; this keeps the QR card invisible to every org except the ones deliberately opted in. Default off, fail-off.
Webhook fails closedGreenWebhookController.authorizedGreen signs nothing (no HMAC equivalent of X-Hub-Signature-256). The only auth is the webhookUrlToken it echoes as Authorization: Bearer. Unset secret ⇒ reject everything. Constant-time compare.
API token never loggedGreenApiService.describeThe token is a path segment in every Green URL, so no raw URL may reach a log line or error message.
Instance teardown on disconnectGreenConnectService.disconnectInstances bill daily from creation. A failed deleteInstanceAccount is logged at error because it leaks money.
STOP keywords shared with the Cloud pathSTOP_KEYWORDS exported from whatsapp-ingestion.service.tsAn opt-out that works on one transport and not the other is a compliance bug (Israeli Amendment 40 covers WhatsApp explicitly).

How it differs from the Cloud API path

Cloud API (whatsapp_cloud)Green (whatsapp_unofficial)
ConnectEmbedded Signup v4 popupPartner createInstance → QR scan → poll getStateInstance
Credentials{ accessToken } (BISU){ idInstance, apiTokenInstance }
externalAccountIdphone_number_ididInstance
TemplatesSubmitted to Meta, approved by reviewLocal only, stored approved, rendered to plain text by renderTemplateText
24h windowEnforced; free-form only inside itNone — WhatsAppWindowService returns true, so automations always send the real body
Message idwamidGreen’s idMessage (same column)
Delivery statusesmessages webhookoutgoingMessageStatus webhook, mapped onto the same applyStatus

Everything downstream of enqueueSend is shared: the whatsapp_messages ledger, the BullMQ queue, broadcasts, notification prefs, lead consent, and the conversations layer.

Precedence during the cutover

WhatsAppSenderService.getActiveConnection prefers whatsapp_cloud whenever both connections are active. The day Meta approves and the org completes Embedded Signup, sending moves to the official transport on its own — no data migration, no flag flip, no code change.

Environment

GREEN_API_PARTNER_TOKEN= # from [email protected]; bills daily per instance GREEN_API_WEBHOOK_BASE_URL= # public API host (locally: cloudflared tunnel) GREEN_API_WEBHOOK_TOKEN= # shared secret; webhook fails closed without it GREEN_API_SEND_DELAY_MS=15000 # anti-ban pacing, range 500..600000 GREEN_API_BASE_URL= # optional host override

Plus the per-org flag whatsapp-green-transport, and whatsapp-messaging for sending as usual.

Code map

PathRole
apps/api/src/whatsapp/green-api.service.tsREST client (send, QR, state, settings, partner provisioning) + GreenApiError
apps/api/src/whatsapp/green-api.config.service.tsEnv reader
apps/api/src/lead-ingestion/green-connect.service.tsProvision → QR → state → disconnect
apps/api/src/lead-ingestion/green-integration.controller.tsAuthed org-scoped connect endpoints
apps/api/src/lead-ingestion/green-webhook.controller.tsPOST /webhooks/green/:connectionId, bearer-authenticated
apps/api/src/lead-ingestion/green-ingestion.service.tsInbound STOP handling
apps/api/src/lead-ingestion/green.types.tsWebhook payload shapes
apps/web/src/components/settings/whatsapp-green-connect.tsxQR pairing card

Green-specific branches inside shared files are marked with a comment naming the transport: whatsapp-sender.service.ts (buildGreenRequestBody), whatsapp-send.processor.ts (deliverViaGreen), whatsapp-events.service.ts (the Green notifications section), whatsapp-templates.service.ts (greenContext), whatsapp-window.service.ts, whatsapp-broadcasts.service.ts.

Removal checklist (when Meta approves)

  1. Turn the whatsapp-green-transport flag off for every org; confirm each gym has completed Embedded Signup.
  2. Disconnect each Green connection through the UI (this deletes the billed instance).
  3. Delete the files in the code map above, plus this doc.
  4. Remove the Green branches in the shared files listed above and the whatsapp_unofficial handling in getActiveConnection.
  5. Drop the GREEN_API_* env vars.

The whatsapp_unofficial value stays in the integration_provider enum — it predates this work (migration 0068) and removing a Postgres enum value is not worth a migration.