Picks up where getting-started.md left off. By the end of this guide you will have:
- Created a child tenant (
acme) with a real admin user. - Provisioned an agent + WebRTC extension for them.
- Registered a softphone (Linphone) and placed an inbound call.
- Verified the call appears in the audit log + analytics.
Reading + execution time: ~30 minutes. Prerequisites: Step 1–5 of getting-started.md completed; you have a
MGMT_KEYin your shell environment from the setup wizard.
Tenants in Verbara.Platform are hierarchical. The host (platform) tenant
owns customer tenants. Use the Management API key to create one:
curl -sf -X POST http://localhost:5000/api/v1/management/tenants \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $MGMT_KEY" \
-d '{
"tenantId": "acme",
"name": "Acme Contact Center",
"type": 2
}'type: 2 = customer tenant (vs. 0 host, 1 reseller). Set its plan so
the Pro feature gates open:
curl -sf -X PUT http://localhost:5000/api/v1/management/tenants/acme/settings \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $MGMT_KEY" \
-d '{"plan": "Pro"}'Pro enables: Dialer, Bot, Analytics export, Flows, Webhooks, Scheduled
reports, Knowledge base, Recordings. Use Enterprise to unlock everything
(Cluster, multi-region, etc.).
We need a JWT for the platform admin to call the per-tenant /admin/*
endpoints. Re-run the login from getting-started.md and capture the token:
PLATFORM_JWT=$(curl -sf -X POST http://localhost:5000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"tenantId":"platform","email":"platform@admin.local","password":"PlatformAdmin2026!"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['accessToken'])")
curl -sf -X POST http://localhost:5000/api/v1/admin/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $PLATFORM_JWT" \
-H "X-Tenant-Id: acme" \
-d '{
"userId": "acme-user-admin",
"email": "admin@acme.local",
"displayName": "Acme Admin",
"role": "Admin",
"password": "AcmeAdmin2026!"
}'
X-Tenant-Idheader. Per-tenant operations always need this header when the calling user belongs to a different tenant. Platform admins can address any descendant tenant; regular tenant users are scoped to their own.
Switch to the Acme admin and create an agent. Agents have a SIP extension
auto-provisioned in the Asterisk Realtime tables — Pro.Realtime handles
this so you do not edit pjsip.conf.
ACME_JWT=$(curl -sf -X POST http://localhost:5000/api/v1/auth/login \
-H "Content-Type: application/json" \
-d '{"tenantId":"acme","email":"admin@acme.local","password":"AcmeAdmin2026!"}' \
| python3 -c "import sys,json; print(json.load(sys.stdin)['accessToken'])")
curl -sf -X POST http://localhost:5000/api/v1/admin/users \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACME_JWT" \
-H "X-Tenant-Id: acme" \
-d '{
"userId": "acme-user-alice",
"email": "alice@acme.local",
"displayName": "Alice (Agent)",
"role": "Agent",
"password": "AcmeAgent2026!"
}'
AGENT=$(curl -sf -X POST http://localhost:5000/api/v1/admin/agents \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACME_JWT" \
-H "X-Tenant-Id: acme" \
-d '{
"userId": "acme-user-alice",
"displayName": "Alice",
"extension": "4001",
"sipPassword": "alice4001"
}')
AGENT_ID=$(echo "$AGENT" | python3 -c "import sys,json; print(json.load(sys.stdin)['id'])")
# Skills are managed via PUT /admin/agents/{id} after create
curl -sf -X PUT "http://localhost:5000/api/v1/admin/agents/$AGENT_ID" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACME_JWT" \
-H "X-Tenant-Id: acme" \
-d '{"skills": ["sales"]}'Verify the extension landed in the Realtime tables:
docker compose -f docker/docker-compose.full.yml exec -T postgres \
psql -U platform -d platform -c \
"SELECT id, callerid, webrtc, transport FROM ps_endpoints WHERE id='4001';"You should see one row with webrtc=yes and transport=transport-wss.
curl -sf -X POST http://localhost:5000/api/v1/admin/queues \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACME_JWT" \
-H "X-Tenant-Id: acme" \
-d '{"name": "Acme Sales", "isActive": true}'
# Re-fetch the queue id (server-assigned). ListQueues returns a PagedResult
# `{items, totalCount, page, pageSize}` and each row has an `id` field.
QUEUE_ID=$(curl -sf -H "Authorization: Bearer $ACME_JWT" -H "X-Tenant-Id: acme" \
http://localhost:5000/api/v1/admin/queues \
| python3 -c "import sys,json; print(next(q['id'] for q in json.load(sys.stdin)['items'] if q['name']=='Acme Sales'))")
# Queue members live at /queues/{queueId}/members (RESTful, R5.1) — not under /admin
curl -sf -X POST "http://localhost:5000/api/v1/queues/$QUEUE_ID/members" \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $ACME_JWT" \
-H "X-Tenant-Id: acme" \
-d "{\"agentId\": \"$AGENT_ID\", \"penalty\": 0}"We will use Linphone (free, cross-platform, WebRTC-friendly). Any SIP softphone supporting WSS works (Zoiper, Bria, MicroSIP, jssip in a browser).
| Field | Value |
|---|---|
| Username | 4001 |
| Password | alice4001 |
| Domain | localhost (or your machine's IP) |
| Transport | TLS (WSS over 8089) — for plain SIP/UDP testing use 5060 |
| Outbound proxy | localhost:8089 (TLS) or localhost:5060 (UDP) |
Self-signed cert. The bundled Asterisk uses a self-signed cert at
:8089. Linphone will warn — accept the cert. For production, swap the cert atdocker/asterisk-config/keys/.
Plain SIP fallback. If TLS gives trouble for testing, register on
5060/udpinstead (Username4001, Passwordalice4001, Domainlocalhost, Transport UDP). Thencore show endpointsfrom the Asterisk CLI to confirm the contact landed.
After registering, the Linphone status should show Registered. If not, peek at the Asterisk CLI:
docker compose -f docker/docker-compose.full.yml exec -T asterisk \
asterisk -rx "pjsip show endpoint 4001"You should see Contact: 4001/sip:... with status Avail.
The bundled dialplan (docker/asterisk-config/extensions.conf, context
default) routes any extension dialed to the matching endpoint. From a
second softphone (or the Asterisk CLI) dial 4001. Alice's softphone
should ring.
To test from the Asterisk CLI:
docker compose -f docker/docker-compose.full.yml exec -T asterisk \
asterisk -rx "channel originate PJSIP/4001 application Playback hello-world"Pick up — you should hear "Hello, world". The call is recorded (default
behavior) under the recordings Docker volume.
In the Web UI (signed in as admin@acme.local on tenant acme):
- Admin → Audit Log. Filter by today. You should see entries for the
tenant create, plan change, user creates, agent create, queue create.
Each row has
actorId(sub claim of the JWT),action,tenantId,timestamp,outcome. Click a row for the diff payload. - Analytics → Recent Calls. Within ~5 seconds the originated call
should appear with
direction=outbound,duration,disposition=ANSWERED. - Analytics → Live Wallboard. Queue snapshot tile should show
Acme SaleswithLoggedIn=1,Availablecount reflecting Alice's state.
If the audit log is empty, check
docker compose -f docker/docker-compose.full.yml logs platform-api --tail=80
for audit errors. The audit sink is wired by default to the
audit_entries Postgres table.
- A two-tenant install (
platformhost +acmecustomer). - One agent (Alice / extension 4001) registered + reachable.
- One queue (
Acme Sales) with Alice as a member. - Working CDR + audit log + recordings.
Continue to first-realistic-demo.md for a guided tour of the R4 + R5 features (multi-tenant analytics, AgentAssist toggle, retention admin, drain demo).
Most often: TLS cert not trusted. Either accept the self-signed cert in Linphone's preferences or fall back to UDP/5060.
RTP ports 20000-20200/udp may not be open in your firewall. On Linux
desktop: sudo ufw allow 20000:20200/udp. In Docker Desktop, ensure the
range is mapped (it is in the bundled compose file).
Pro.Realtime's reconciler runs every 30s. The simplest remedy is to wait ~30s
and re-list with
curl -sf -H "Authorization: Bearer $ACME_JWT" -H "X-Tenant-Id: acme" "http://localhost:5000/api/v1/queues/$QUEUE_ID/members".
If you need to force an immediate sync, restart the API container —
docker compose -f docker/docker-compose.full.yml restart platform-api — which
runs the reconciler at startup. (A first-class POST /admin/realtime/reconcile
admin endpoint is on the R5.4 backlog.)