The gateway ships with a built-in OpenAI-compatible provider. Any endpoint that speaks the OpenAI /chat/completions shape works (OpenAI itself, OpenRouter, Groq, OpenAI-compatible local servers, Azure-OpenAI proxies, etc.).
1. Global — applies to every account by default. Edit at /ai.
2. Per-account — overrides for one WhatsApp account. Edit at /accounts/:id.
3. Per-chat — inherit | enabled | disabled toggle plus an optional custom system prompt. Edit by tapping the gear icon in any chat detail page.
Effective config is resolved at runtime — chat → account → global → env.
curl -X PUT "$PUBLIC_API_URL/v1/ai/global" \
-H "Authorization: Bearer $ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{
"enabled": true,
"base_url": "https://api.openai.com/v1",
"api_key": "sk-...",
"model_name": "gpt-4.1-mini",
"default_system_prompt": "You are a helpful WhatsApp assistant.",
"temperature": 0.3,
"max_tokens": 700,
"timeout_seconds": 60,
"reply_delay_seconds": 2,
"reply_to_private": true,
"reply_to_groups": false,
"reply_to_channels": false,
"echo_mode": false
}'
The api_key is encrypted at rest. Reads return has_api_key: true/false, never the raw key.
1. Incoming message arrives → persisted in messages table.
2. Worker re-evaluates the rules (global enabled? account enabled? chat enabled? not blocked?).
3. Builds a conversation: system prompt + last 20 messages.
4. Calls ${base_url}/chat/completions.
5. Persists ai_log row, emits ai.reply.generated webhook.
6. Sleeps reply_delay_seconds, simulates typing, sends as the WhatsApp account.
7. Emits ai.reply.sent (or ai.reply.failed).
If api_key is missing the message is logged as skipped with reason no API key configured and never sent.
curl -X POST "$PUBLIC_API_URL/v1/ai/chat/test" \
-H "Authorization: Bearer YOUR_ACCOUNT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"message": "Hello, can you help me?",
"chat_id": "optional-chat-id",
"send_to_chat": false
}'
When send_to_chat: true is set and chat_id is present, the AI reply is also sent through the WhatsApp session.
The dashboard's AI chat / test page uses this:
curl -X POST "$PUBLIC_API_URL/v1/ai/admin/test" \
-H "Authorization: Bearer $ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{
"account_id": "optional",
"chat_id": "optional",
"message": "Say hi"
}'
curl -X PUT "$PUBLIC_API_URL/v1/accounts/ACC/chats/CHAT_JID/ai" \
-H "Authorization: Bearer $ADMIN_JWT" \
-H "Content-Type: application/json" \
-d '{"mode":"disabled","custom_system_prompt":null}'
Modes: inherit, enabled, disabled.
OPENAI_API_KEY or ENCRYPTION_KEY to git.ENCRYPTION_KEY only when you can re-enter every encrypted API key — rotating it invalidates existing ciphertext.api_key value.timeout_seconds); slow endpoints won't block the queue forever.