Concepts

The CallingScout object model + how the pieces fit together.

Tenants

One tenant per account. Every resource (agent, call, campaign, number, webhook, API key) lives under exactly one tenant. Tenants are created lazily on first Clerk sign-in (dev) or explicitly by the webhook on Clerk org creation (production).

API keys

Authenticate every REST call. Prefixed sk_live_ (production) or sk_test_ (sandbox). Bcrypt-hashed on the server — we show the plaintext once on creation, never again. Pass as Authorization: Bearer <key>.

See the API-key section of the quickstart for creation flow + sandbox semantics.

Agents

An agent is a configured AI voice persona: pipeline type (cascaded | realtime), provider choices (STT / LLM / TTS or S2S), system prompt, first message, tool list, kill switch (is_active). Agents are cheap to create + edit; changes take effect on the next call.

The built-in library ships 17 templates across HR, sales, healthcare, insurance, collections, support — GET /api/v1/agent-templates. Clone a template into your own agent, tweak, ship.

Calls

A call is one dial attempt. Created by POST /api/v1/calls (outbound) or the provider webhook (inbound). Walks through:

QUEUED → RINGING → IN_PROGRESS → COMPLETED
                                 ├─→ FAILED
                                 ├─→ BUSY
                                 ├─→ NO_ANSWER
                                 └─→ VOICEMAIL

Every call is gated by the compliance layer before dispatch. On success you get the full post-call bundle: transcript, recording, per-service cost breakdown, outcome classification.

Campaigns

A campaign dispatches an agent against a list of contacts at a configured rate. Uploaded via CSV, paced via a Redis token bucket (max_concurrent + calls_per_minute), scoped to the agent's calling hours. Stale-contact recovery restarts dialing on worker restart.

Lifecycle webhooks: campaign.started, campaign.contact.completed, campaign.completed.

Phone numbers

Numbers provisioned through CallingScout are bought from Twilio or Telnyx on your behalf and are automatically caller-ID verified + STIR/SHAKEN level A attested. You dial from them the moment POST /api/v1/phones/provision returns.

If you want to use a number you already own as the from_number, go through the OTP verification flow first. Outbound dispatch refuses unverified from_numbers and any number attested at STIR/SHAKEN level B or C.

Compliance gate

Every outbound call passes through check_compliance() before dispatch — DNC registry, TCPA calling hours, two-party consent state detection, capacity, AI first-utterance disclosure. Results are persisted to compliance_checks; a Postgres trigger refuses to INSERT a call that references a denied check. Not bypassable from the application layer even if a future bug tried to.

See /security for the full posture, and compliance/gate.py for the code.

Webhooks

Outbound event delivery to your systems. 15 event types across call lifecycle, post-call artifacts, campaigns, agents, usage. Per-subscription signing secret, at-least-once delivery, 30-day replay history, test-fire for CI. See docs/webhooks.md.

Sandbox mode

A mode toggle built into the API key itself. sk_test_ keys produce sandbox requests:

  • Calls dispatched in sandbox mark is_test_call = true on the calls row.
  • Dispatcher routes to Twilio Magic Numbers instead of real PSTN.
  • Billing skips Stripe meter emission. Sandbox calls are always $0.
  • Webhooks only fire to subscriptions that declared sandbox: true. Production subs never receive sandbox events, and vice versa.
  • Analytics excludes sandbox by default (toggle to show).

Use for CI, load tests against fixtures, and local dev without burning real minutes.

Idempotency

Every mutating endpoint (POST/PATCH/PUT/DELETE) accepts an Idempotency-Key header. On a retry within 24 hours with the same key, we return the cached response with X-Idempotent-Replay: true — your handler doesn't re-run. Pattern matches Stripe. The SDKs auto-generate a UUID per request unless you override.

Rate limits

Per-tenant, per-endpoint-class. Current classes:

ClassDefault limitCovered endpoints
default60 req/minGET, list, detail endpoints
dial10 req/minPOST /calls, POST /phones/provision
campaign-dispatch120 req/mincampaign engine's internal dispatch path

Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset. 429 includes Retry-After (seconds).

Pagination

List endpoints support cursor-based pagination. Pass ?cursor=<opaque> to continue. Response includes has_more and next_cursor. Cursor is O(1) and stable under writes. Don't parse the cursor; it's opaque.

Versioning

Path-versioned at /v1/. Breaking changes bump to /v2/; /v1/ is supported for at least 12 months after /v2/ ships. SDKs track API major via their own semver — callingscout@1.x.y speaks /v1/.

Things that are NOT in the public API

By design:

  • Provider-inbound webhooks/api/v1/webhooks/twilio, /api/v1/webhooks/telnyx, /api/v1/webhooks/stripe. Signed by the provider, intended only for provider → us.
  • Clerk sync/api/v1/clerk/webhook.
  • Operator controls — internal whisper / nudge / hangup / listen.
  • LLM proxy — internal routing shim for the dashboard's scout drafts.
  • Live-call stream — internal WebSocket for the operator console.

These routes exist and work, but they're excluded from the public OpenAPI. The SDKs don't expose them.