Quickstart — zero to first call
This guide takes you from an empty machine to your first compliance-gated call attempt, end to end, against the local stack — the same docker-compose harness the platform's own e2e suite runs on. Every call it places is real (SIP through Kamailio/FreeSWITCH, real CDR rows, real compliance evidence); the only mock is the carrier at the far end, so no actual phone rings and nothing costs money.
You will: bring the stack up → get an API token → create a campaign → import debts → put an agent seat online → watch the dialer place the first call attempt → read the durable result and the compliance verdict.
Total time: ~1 hour on the first run (most of it is a one-time image build); ~15 minutes on subsequent runs.
Copy-paste friendly
Every step is a runnable curl. Examples use jq to capture ids into shell variables — if you don't have it, copy the ids by hand from the JSON responses.
Prerequisites (~10 min)
- Linux (or macOS) with Docker + docker compose v2,
make,git,curl. - A GitHub account with access to the
DialerDigitalrepositories and a token withread:packages(the harness pulls the published FreeSWITCH image from GHCR). - Free host ports:
4000(core API),5060/5061/5070/5080/8021/8443/22222(telecom plane),55432/56379/55060/58080/58090(mocks). A softphone already running on the host grabs5060— move it or close it.
The compose file builds sibling repos from source, so clone them side by side:
mkdir dd && cd dd
git clone https://github.com/DialerDigital/integration-tests.git
git clone https://github.com/DialerDigital/core.git
git clone https://github.com/DialerDigital/kamailio.git
git clone https://github.com/DialerDigital/rtpengine.git
git clone https://github.com/DialerDigital/dashboard.git
docker login ghcr.io # PAT with read:packages, for the FreeSWITCH image
cd integration-testsStep 1 — Bring the stack up (~10–30 min first run, ~2 min after)
make up-telecomThis starts the mocks (Postgres, Dragonfly, a SIPp mock carrier, STT/TTS), builds and boots the telecom plane (Kamailio, rtpengine, FreeSWITCH, core), runs core's database migrations, and waits until the API answers. The first run compiles the rtpengine image from source — expect 10–30 minutes. Subsequent runs hit the Docker layer cache.
Verify:
make smoke
curl -s http://127.0.0.1:4000/healthz
# -> {"status":"ok","service":"dialer-core","version":"..."}The owner dashboard also comes up, at http://127.0.0.1:8080 — handy for watching the live floor in a browser while you run the steps below.
Step 2 — Get your API token (~1 min)
Locally you mint the first tenant key with the idempotent seed (in production the first key is issued at onboarding — see Authentication). The seed creates the demo tenant local-dev, its API key, an agent seat at SIP extension 1000, and a small demo campaign:
docker compose --profile mocks --profile telecom run --rm --no-deps \
-e DATABASE_USER=dialer -e DATABASE_PASSWORD=testpass \
-e SEED_API_TOKEN=localdev-m2-demo-token \
core-migrate eval 'Dialer.Release.seed()'Pinning SEED_API_TOKEN makes the token reproducible across re-runs. Export your session variables:
API=http://127.0.0.1:4000
TOKEN=localdev-m2-demo-tokenSanity-check the key — GET /v1/me returns the authenticated tenant:
curl -s $API/v1/me -H "Authorization: Bearer $TOKEN"
# -> {"tenant":{"id":"...","name":"local-dev","status":"active",...}}401 unauthorized here means the seed didn't run or the token doesn't match — re-run the seed command above.
Step 3 — Create a campaign (~1 min)
Campaigns are created in draft and started explicitly. Two lab-specific settings, both honest about what they do:
calling_window_*wide open — this is campaign policy, not the law: the compliance engine's quiet-hours gate ([08:00, 21:00) in the consumer's timezone) still enforces independently on every dial.abandonment_threshold: 0.9— if you run headless (no softphone answering, next step), bridges to the agent fail and count asabandoned; the real-world default of 3% would freeze pacing after the first one. Keep the default in any real deployment.
CID=$(curl -s -X POST $API/v1/campaigns \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "quickstart-'$(date +%s)'",
"calling_window_start": "00:00:00",
"calling_window_end": "23:59:59",
"default_timezone": "America/Chicago",
"abandonment_threshold": 0.9,
"caller_ids": ["+13125550199"]
}' | jq -r .campaign.id)
echo $CIDcampaign_type defaults to predictive — the runner paces and dials on its own once started. See the campaign schema for every field.
Step 4 — Import debts (~2 min)
Leads enter through the bulk JSON import — idempotent (debts upsert on external_ref), per-item fail-soft. We import two on purpose:
- a landline contact the engine may dial, and
- a mobile contact with no TCPA consent on file, which the engine must refuse — that refusal is your first compliance verdict.
One thing to get right: the quiet-hours gate reads the contact's timezone. Pick one where it is currently between 08:00 and 21:00 local time:
TZ=America/Chicago date +%H:%M # inside 08:00-21:00? use it. Otherwise try
TZ=Europe/Madrid date +%H:%M # ...or Asia/Tokyo, Pacific/Auckland, etc.
LEAD_TZ=America/Chicago # <- your pickcurl -s -X POST $API/v1/debts/import \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"debts": [
{
"external_ref": "quickstart-0001",
"consumer_ref": "quickstart-consumer-1",
"debt_type": "other",
"amount_cents": 50000,
"currency": "USD",
"campaign_id": "'$CID'",
"contacts": [
{ "phone_e164": "+13125551001", "line_type": "landline",
"timezone": "'$LEAD_TZ'", "us_state": "IL", "is_primary": true }
]
},
{
"external_ref": "quickstart-0002",
"consumer_ref": "quickstart-consumer-2",
"debt_type": "other",
"amount_cents": 75000,
"currency": "USD",
"campaign_id": "'$CID'",
"contacts": [
{ "phone_e164": "+13125551002", "line_type": "mobile",
"timezone": "'$LEAD_TZ'", "us_state": "IL", "is_primary": true }
]
}
]
}'
# -> {"imported":2,"failed":[]}external_ref is the Reg F frequency-counter key — keep it stable and unique per account. Details in the REST API guide.
Optional sanity check — preview the next dialable lead without dialing:
curl -s $API/v1/campaigns/$CID/next-lead -H "Authorization: Bearer $TOKEN"
# -> {"lead":{"debt_ref":"quickstart-0001","contact_phone":"+13125551001",...}}Step 5 — Put an agent seat online (~1 min)
The seed already created a seat at SIP extension 1000. Check it in so connected calls have somewhere to bridge:
curl -s -X POST $API/v1/agents/checkin \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"sip_extension": "1000", "device_mode": "external"}'
# -> {"agent":{...,"presence":"available"}, "sip":{...}}Two ways to run the next step:
- Headless (no audio, fully scripted). Don't register any device. The carrier leg still completes; the bridge to the seat fails and the attempt records as
abandoned. You still get a durable CDR row with its compliance snapshot — fine for a first run. - Hear it. Register a SIP softphone as
1000/ passwordlocaldev123against the FreeSWITCH internal profile at<your-host-LAN-IP>:5070, then answer when it rings — the attempt records asanswered_human. Setup details in the harness'sREADME-LOCAL.md("Zoiper setup").
Step 6 — Start the campaign and watch the first call (~3 min)
curl -s -X POST $API/v1/campaigns/$CID/start \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"gateway": "default", "batch_size": 2, "tick_ms": 1000}'
# -> {"campaign":{...,"state":"running"}}gateway: "default" is the local stack's mock-carrier trunk: the runner sends a real ESL originate to FreeSWITCH, which dials the SIPp mock carrier — it answers, plays ~3 s of media, and hangs up. Poll the live counters (the same payload streams every ~2 s over the WebSocket live floor):
curl -s $API/v1/campaigns/$CID/stats -H "Authorization: Bearer $TOKEN"Within ~20 seconds you should see dialed ≥ 1 (the landline lead) and blocked ≥ 1 (the mobile lead the consent gate refused). If dialed stays 0 and blocked climbs, your lead timezone is outside quiet hours — go back to Step 4.
Step 7 — Read the result and the compliance verdict (~3 min)
The CDR row — append-only, one row per attempt, with the compliance decision frozen at dial time:
curl -s "$API/v1/call_attempts?campaign_id=$CID&limit=10" \
-H "Authorization: Bearer $TOKEN"Look at the first attempt: disposition (answered_human if a softphone answered, abandoned on the headless path), started_at/answered_at/ended_at, hangup_cause, and — the part that matters in an audit — compliance_snapshot: the frozen evidence of the decision that allowed this dial, immune to later data changes.
The refusal evidence — the mobile lead was never dialed, and that refusal is a durable row too, written before the lead was skipped:
curl -s "$API/v1/stats/blocked?campaign_id=$CID" -H "Authorization: Bearer $TOKEN"
# -> {"total":1,"by_gate":{"tcpa_no_consent":1},"daily":[...]}That pair — the call you made with its frozen snapshot, and the call you didn't make with its reason — is the platform's evidence model in miniature. See Errors & compliance and the compliance evidence endpoints (violations-prevented report, per-debt defense packet).
Step 8 — Wind down (~1 min)
curl -s -X POST $API/v1/campaigns/$CID/stop -H "Authorization: Bearer $TOKEN"
curl -s -X POST $API/v1/agents/checkout \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"sip_extension": "1000"}'
make down # stop the whole stack (state persists in the compose volumes)Troubleshooting
| Symptom | Likely cause / fix |
|---|---|
401 unauthorized on every call | Seed not run, or TOKEN doesn't match SEED_API_TOKEN — re-run Step 2. |
stats is null on campaign stats | The runner isn't up — did POST .../start return 200? |
dialed: 0, blocked climbing | Quiet-hours gate: lead timezone outside 08:00–21:00 local. Re-import with a daytime timezone (blocked leads retry after a cooldown, 5 min by default). |
| No CDR rows at all | Core may not have an ESL link to FreeSWITCH yet — make logs, look for "ESL ready". |
make up-telecom fails on ports | Something owns a telecom port: sudo ss -lunp | grep -E ':5060|:5070|:5080|:8021'. |
| FreeSWITCH image pull fails | docker login ghcr.io with a PAT that has read:packages. |
Where to go next
- Authentication — key rotation, WebSocket auth, tenancy model.
- REST API guide — typed dispositions (record a promise-to-pay on your first call), callbacks, supervision, DIDs.
- WebSocket events — replace the polling in Step 6 with push.
- The harness's
make demo— a scripted end-to-end demo of the full sellable loop on this same stack.
Production endpoints
Everything above maps 1:1 to production with three differences:
https://api.usa.dialerdigital.com # production (USA cell)- Keys: production API keys (
dd_...) are issued at onboarding and rotated by you via/v1/api_keys— there is no seed. - Carrier: no mock — calls go out through the carrier configuration provisioned on your account, to real numbers.
- Stakes: the same compliance gates run, but against real consumers. Lab shortcuts in this guide (wide calling window, 0.9 abandonment threshold) have no place there.