Skip to content

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 DialerDigital repositories and a token with read: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 grabs 5060 — move it or close it.

The compose file builds sibling repos from source, so clone them side by side:

bash
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-tests

Step 1 — Bring the stack up (~10–30 min first run, ~2 min after)

bash
make up-telecom

This 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:

bash
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:

bash
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:

bash
API=http://127.0.0.1:4000
TOKEN=localdev-m2-demo-token

Sanity-check the key — GET /v1/me returns the authenticated tenant:

bash
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 as abandoned; the real-world default of 3% would freeze pacing after the first one. Keep the default in any real deployment.
bash
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 $CID

campaign_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:

  1. a landline contact the engine may dial, and
  2. 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:

bash
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 pick
bash
curl -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:

bash
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:

bash
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 / password localdev123 against the FreeSWITCH internal profile at <your-host-LAN-IP>:5070, then answer when it rings — the attempt records as answered_human. Setup details in the harness's README-LOCAL.md ("Zoiper setup").

Step 6 — Start the campaign and watch the first call (~3 min)

bash
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):

bash
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:

bash
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:

bash
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)

bash
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

SymptomLikely cause / fix
401 unauthorized on every callSeed not run, or TOKEN doesn't match SEED_API_TOKEN — re-run Step 2.
stats is null on campaign statsThe runner isn't up — did POST .../start return 200?
dialed: 0, blocked climbingQuiet-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 allCore may not have an ESL link to FreeSWITCH yet — make logs, look for "ESL ready".
make up-telecom fails on portsSomething owns a telecom port: sudo ss -lunp | grep -E ':5060|:5070|:5080|:8021'.
FreeSWITCH image pull failsdocker 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.

Nothing in these docs is legal advice — always confirm compliance posture with your own counsel.