Chatwoot Wrapper — End-to-End Testing Guide
Complete guide to test the Chatwoot wrapper extension from zero to working conversations.
Prerequisites
- Java 25+, Maven installed
- Docker + Docker Compose installed
- Ports available: 3000 (Chatwoot), 5433 (Postgres), 6380 (Redis), 8081 (Wrapper)
Phase 1: Start Chatwoot + Run Setup
# From the repo root — start infrastructure
docker compose -f chatwoot-extension/docker/docker-compose.yml up -dWait ~30s for migration to complete. Verify all containers are up:
docker compose -f chatwoot-extension/docker/docker-compose.yml psAll services should show Up (except chatwoot-migrate which exits after completing).
Now run the automated setup script. It creates the SuperAdmin, PlatformApp token, and agent user in one go:
bash chatwoot-extension/docker/setup.shOn success you'll see output like:
=== Chatwoot Initial Setup ===
Step 1: Creating SuperAdmin user...
SuperAdmin (created): admin@metaone.local
Step 2: Creating PlatformApp for API access...
Platform token: qYdvVLWb7sgdJNfud7XuskmW
Platform API verified.
Step 3: Creating agent user for Application API...
Agent user ID: 2
Agent token: 8fXT4b8PL3Gv8s9sfkfJ7CrZ
=== Setup Complete ===Save those values:
export CW_PLATFORM_TOKEN=<platform token from Step 2>
export CW_AGENT_TOKEN=<agent token from Step 3>What the setup script does:
- Creates a SuperAdmin user (for Chatwoot web admin access)
- Creates a PlatformApp and its token (the Platform API at
/platform/api/v1/*requires a PlatformApp token, NOT a user access token)- Creates an agent user via the Platform API (this user's access token is needed for the Application API: conversations, messages, contacts)
Phase 2: Build & Start the Wrapper
# Build (from repo root)
mvn -pl chatwoot-extension clean package -DskipTests -q
# Make sure port 8081 is free
lsof -ti:8081 | xargs kill 2>/dev/null; true
# Start the wrapper
CHATWOOT_PLATFORM_TOKEN=$CW_PLATFORM_TOKEN \
mvn -pl chatwoot-extension spring-boot:runOpen a new terminal for the remaining test commands (re-export the env vars there).
Verify the wrapper is running:
curl -s http://localhost:8081/health | python3 -m json.toolExpected:
{
"status": "UP",
"chatwoot": "reachable"
}Phase 3: Test Widget Config (Account Provisioning)
This creates a Chatwoot account for your workspace on first access.
curl -s -H "X-Workspace-Id: ws-demo" http://localhost:8081/widget/config | python3 -m json.toolExpected:
{
"chatwootBaseUrl": "http://localhost:3000",
"identityValidationEnabled": false,
"accountId": 1
}What happened: The wrapper auto-provisioned a Chatwoot account for workspace ws-demo via the Platform API and stored the mapping in its H2 database.
Phase 4: Provision Agent into the Account
The wrapper created a Chatwoot account in Phase 3, but the capability endpoints (conversations, messages) use Chatwoot's Application API, which needs an agent user assigned to that account.
Step 4a: Get the account ID
curl -s http://localhost:3000/platform/api/v1/accounts \
-H "api_access_token: $CW_PLATFORM_TOKEN" | python3 -m json.toolNote the account id (likely 1).
export CW_ACCOUNT_ID=1 # adjust if differentStep 4b: Add the agent user to the account
The setup script already created an agent user. Now add them to the account:
# Get agent user ID (printed by setup.sh, or find it)
export CW_AGENT_USER_ID=<agent user ID from setup.sh>
curl -s -X POST "http://localhost:3000/platform/api/v1/accounts/$CW_ACCOUNT_ID/account_users" \
-H "Content-Type: application/json" \
-H "api_access_token: $CW_PLATFORM_TOKEN" \
-d "{
\"user_id\": $CW_AGENT_USER_ID,
\"role\": \"administrator\"
}" | python3 -m json.toolStep 4c: Verify the agent token works
curl -s "http://localhost:3000/api/v1/accounts/$CW_ACCOUNT_ID/conversations" \
-H "api_access_token: $CW_AGENT_TOKEN" | python3 -m json.toolExpected: {"data": {"meta": {...}, "payload": []}} (empty conversations list).
Phase 5: Test Conversations Against Chatwoot API
Test the full conversation lifecycle directly against Chatwoot's API.
Create an inbox (conversations need one)
curl -s -X POST "http://localhost:3000/api/v1/accounts/$CW_ACCOUNT_ID/inboxes" \
-H "Content-Type: application/json" \
-H "api_access_token: $CW_AGENT_TOKEN" \
-d '{
"name": "MetaOne Chat",
"channel": {
"type": "api",
"webhook_url": "http://host.docker.internal:8081/webhooks/chatwoot"
}
}' | python3 -m json.toolexport CW_INBOX_ID=<inbox id from response>Create a contact
curl -s -X POST "http://localhost:3000/api/v1/accounts/$CW_ACCOUNT_ID/contacts" \
-H "Content-Type: application/json" \
-H "api_access_token: $CW_AGENT_TOKEN" \
-d '{
"name": "Test User",
"email": "testuser@example.com"
}' | python3 -m json.toolexport CW_CONTACT_ID=<contact id from response>Create a conversation
curl -s -X POST "http://localhost:3000/api/v1/accounts/$CW_ACCOUNT_ID/conversations" \
-H "Content-Type: application/json" \
-H "api_access_token: $CW_AGENT_TOKEN" \
-d "{
\"source_id\": \"metaone-test-$(date +%s)\",
\"inbox_id\": $CW_INBOX_ID,
\"contact_id\": $CW_CONTACT_ID,
\"additional_attributes\": {\"name\": \"Test Conversation\"}
}" | python3 -m json.toolexport CW_CONVERSATION_ID=<conversation id from response>Send a message
curl -s -X POST "http://localhost:3000/api/v1/accounts/$CW_ACCOUNT_ID/conversations/$CW_CONVERSATION_ID/messages" \
-H "Content-Type: application/json" \
-H "api_access_token: $CW_AGENT_TOKEN" \
-d '{
"content": "Hello from MetaOne testing!",
"message_type": "outgoing"
}' | python3 -m json.toolList messages
curl -s "http://localhost:3000/api/v1/accounts/$CW_ACCOUNT_ID/conversations/$CW_CONVERSATION_ID/messages" \
-H "api_access_token: $CW_AGENT_TOKEN" | python3 -m json.toolList conversations
curl -s "http://localhost:3000/api/v1/accounts/$CW_ACCOUNT_ID/conversations" \
-H "api_access_token: $CW_AGENT_TOKEN" | python3 -m json.toolPhase 6: Test Widget Identity Token
# Without identity validation token (returns empty hash)
curl -s http://localhost:8081/widget/identity-token \
-H "X-Workspace-Id: ws-demo" \
-H "X-Actor-Id: user-42" | python3 -m json.toolExpected:
{
"identifier": "user-42",
"identityHash": ""
}To test HMAC generation, restart the wrapper with a validation token:
# Stop wrapper (Ctrl+C), then restart with:
CHATWOOT_PLATFORM_TOKEN=$CW_PLATFORM_TOKEN \
CHATWOOT_IDENTITY_VALIDATION_TOKEN=my-secret-key \
mvn -pl chatwoot-extension spring-boot:runThen:
curl -s http://localhost:8081/widget/identity-token \
-H "X-Workspace-Id: ws-demo" \
-H "X-Actor-Id: user-42" | python3 -m json.toolExpected: a non-empty identityHash (HMAC-SHA256 hex string).
Phase 7: Test Webhook Bridge (Chatwoot → Wrapper)
Register a webhook in Chatwoot
curl -s -X POST "http://localhost:3000/api/v1/accounts/$CW_ACCOUNT_ID/webhooks" \
-H "Content-Type: application/json" \
-H "api_access_token: $CW_AGENT_TOKEN" \
-d '{
"url": "http://host.docker.internal:8081/webhooks/chatwoot",
"subscriptions": ["message_created", "conversation_created", "conversation_status_changed"]
}' | python3 -m json.tool
host.docker.internallets Docker containers reach your Mac's localhost.
Trigger a webhook event
Send a message (Phase 5) and check the wrapper terminal output.
You should see: Received Chatwoot webhook event: message_created
The webhook tries to forward events to MetaOne Core at
http://localhost:8080/api. If Core isn't running, you'll see a log error. That's expected.
Phase 8: Test Event Forwarding (MetaOne → Wrapper)
curl -s -X POST http://localhost:8081/events \
-H "Content-Type: application/json" \
-H "X-Workspace-Id: ws-demo" \
-d '{
"eventKey": "platform.notification",
"payload": {
"channel": "general",
"message": "New deal closed: Acme Corp $50k",
"sender": "crm-bot"
}
}'Check wrapper logs for: Forwarding notification to Chatwoot: [crm-bot] New deal closed...
Note: The actual message delivery to a Chatwoot conversation is a TODO (needs channel-to-conversation mapping). The event is received and logged.
Quick Reference: All Endpoints
| Endpoint | Method | Headers | Purpose |
|---|---|---|---|
/health | GET | — | Health check |
/widget/config | GET | X-Workspace-Id | Widget SDK config |
/widget/identity-token | GET | X-Workspace-Id, X-Actor-Id | HMAC identity hash |
/capabilities/chat.conversation.create | POST | X-Workspace-Id | Create conversation |
/capabilities/chat.conversation.get | POST | X-Workspace-Id | Get conversation by ID |
/capabilities/chat.conversation.list | POST | X-Workspace-Id | List conversations |
/capabilities/chat.message.send | POST | X-Workspace-Id | Send a message |
/capabilities/chat.message.list | POST | X-Workspace-Id | List messages |
/capabilities/chat.member.add | POST | X-Workspace-Id | Add participant |
/capabilities/chat.member.remove | POST | X-Workspace-Id | Remove participant |
/webhooks/chatwoot | POST | — | Chatwoot webhook receiver |
/events | POST | X-Workspace-Id | MetaOne event receiver |
Chatwoot Token Types
| Token | Created from | Used for | API prefix |
|---|---|---|---|
| PlatformApp token | PlatformApp model | Platform API (accounts, users) | /platform/api/v1/* |
| User access token | User model | Application API (conversations, messages) | /api/v1/* |
| SuperAdmin token | SuperAdmin user | Chatwoot admin web UI | N/A (web login) |
The Platform API and Application API use the same header (api_access_token) but require different token types. This is the most common source of 401 errors.
Environment Variables
| Variable | Default | Purpose |
|---|---|---|
CHATWOOT_BASE_URL | http://localhost:3000 | Chatwoot instance URL |
CHATWOOT_PLATFORM_TOKEN | (required) | PlatformApp API token |
CHATWOOT_WEBHOOK_SECRET | (empty) | Webhook signature validation |
CHATWOOT_IDENTITY_VALIDATION_TOKEN | (empty) | HMAC key for widget identity |
METAONE_CORE_URL | http://localhost:8080/api | MetaOne Core base URL |
Cleanup
# Stop wrapper: Ctrl+C in that terminal, or:
lsof -ti:8081 | xargs kill
# Stop Chatwoot stack (preserves data in Docker volumes):
docker compose -f chatwoot-extension/docker/docker-compose.yml down
# Full cleanup (destroys all data):
docker compose -f chatwoot-extension/docker/docker-compose.yml down -vKnown Issues
Wrapper capability endpoints return 401 —
provisionWorkspace()creates a Chatwoot account but doesn't provision an agent user into it. The fix: after creating the account, also create a user viaPOST /platform/api/v1/users, add them to the account viaPOST /platform/api/v1/accounts/{id}/account_users, and store that user'saccess_tokenas theagentTokenin the workspace mapping.Event forwarding is incomplete — The
EventControllerreceivesplatform.notificationevents but doesn't yet mapchannelnames to Chatwoot conversation IDs.Conversation create needs inbox_id — Chatwoot requires an
inbox_idto create conversations. The wrapper should auto-create an API inbox per workspace during provisioning and store that ID.