DialerBee APIv1 · 1.0.0-rc6

Python SDK

The official Python client for the DialerBee API, version 1: leads in, results out.

Server-side only. An API key can read your call records and change your campaigns. Keep it in your server's secret store. Never put it in a browser, mobile or desktop app, and never commit it.

Install

pip install dialerbee          # after the first release; for now: pip install ./developer/sdk-python

Authenticate

Create an API client and a key in the DialerBee console. A key looks like dbk_live_<credential id>_<secret> and is shown once. Legacy console keys (eng_live_...) do not work with this API.

export DIALERBEE_API_KEY='dbk_live_...'
from dialerbee import DialerBee

db = DialerBee()                  # or DialerBee(api_key=...)
me = db.me.get()                  # works with any active key, even one with no scopes
print(me.client.name, me.account.name, me.scopes)

The SDK sends the key only in the Authorization: Bearer header. It never puts the key in a URL, a log line, an exception or a repr(), and a client object cannot be pickled. dbk_test_ keys answer 401 invalid_api_key until the sandbox is enabled for your account.

Import leads

Leads go into a draft, ready or paused campaign through the platform's own validation (phone checks, duplicates, field rules, Do-Not-Call screen). At most 200 leads per request.

result = db.leads.import_(
    campaign_id="cmp_0f8fad5b-d9cb-469f-a165-70867728950e",
    leads=[
        {"phone": "+12125550101", "external_id": "CRM-1", "full_name": "Ann Lee"},
        {"phone": "+12125550102", "external_id": "CRM-2", "custom_fields": {"tier": "gold"}},
    ],
    idempotency_key="crm-batch-2026-09-29-001",   # your own id for this batch
)
print(result.summary.accepted, "accepted of", result.summary.received)
for row in result.data:
    if row.status != "accepted":
        print(row.index, row.external_id, row.status, row.reason)

Each row has its own status (accepted, duplicate, on_dnc, invalid, blocked_country, failed). A lead's phone number is never changed by leads.update; import the new number instead.

Lists and pagination

Every list returns one page. Iterate the page to walk all pages; the SDK repeats your filters with the opaque cursor and stops with an error if a cursor ever repeats.

page = db.calls.list(direction="outbound", limit=100)
print(len(page.data), page.has_more)

for call in page:                 # this page, then every following page
    print(call.id, call.status, call.duration_seconds)

calls.list returns every call of the account, from every team, campaign and agent.

Calls, recordings, transcripts

call = db.calls.get("call_0f8fad5b-d9cb-469f-a165-70867728950e")

rec = db.calls.recording(call.id)                      # whole file in memory
rec.write_to("call.mp3")

with db.calls.stream_recording(call.id) as stream:     # large files
    with open("call.mp3", "wb") as fh:
        for chunk in stream.iter_bytes():
            fh.write(chunk)

part = db.calls.recording(call.id, byte_range="bytes=0-65535")   # status_code 206

transcript = db.calls.transcript(call.id)
summary = db.calls.summary(call.id)                    # needs the summaries:read scope
if summary.status == "ready":
    print(summary.summary, summary.next_action)
elif summary.status == "not_generated":
    print("no summary:", summary.not_generated_reason)  # e.g. not_sampled, disabled, expired

Every call of your account answers a summary with a status: ready, processing or not_generated (the reason is in not_generated_reason; treat a reason you do not know as "not generated"). Calls handled by people and calls handled by an AI agent both have one. Answers may gain fields within v1: the SDK keeps a field it does not know (read it as an attribute) and never fails on it. A request with a field the API does not know is refused before anything is sent.

Campaigns

progress = db.campaigns.progress("cmp_0f8fad5b-d9cb-469f-a165-70867728950e")
db.campaigns.pause("cmp_0f8fad5b-d9cb-469f-a165-70867728950e")      # reduces contact: Tier 1

Webhooks

endpoint = db.webhooks.create(url="https://example.com/dialerbee", events=["call.completed"])
secret = endpoint.secret          # shown once: store it in your secret store

The signing secret is in the first answer only: DialerBee never stores the secret. If the same call is replayed (same idempotency_key; endpoint.response_info.idempotency_replayed is True), secret is None. The same holds for db.webhooks.rotate_secret(...). If you lost the secret, rotate it.

Verify every delivery with the exact raw request body, before parsing it:

from dialerbee.webhooks import construct_event, WebhookSignatureError

def handle(raw_body: bytes, headers: dict) -> int:
    try:
        event = construct_event(raw_body, headers, secrets=[secret])
    except WebhookSignatureError as e:
        return 400                                  # e.reason says why
    if already_processed(event.id):                 # deliveries are at-least-once
        return 204
    if event.type == "call.completed":
        print(event.data.call_id, event.data.duration_seconds)
    return 204

A delivery is refused when it was signed more than 300 seconds from your clock (tolerance= changes it). After db.webhooks.rotate_secret(...) DialerBee signs with both secrets for 24 hours, so you can switch at your own pace: pass secrets=[new, old] during the switch. Unknown event types come back as WebhookEnvelopeBase; unknown fields are kept. dialerbee.webhooks.sign() builds a valid header to test your receiver.

Errors

Errors are RFC 9457 problems turned into exceptions. Branch on status_code and code, never on detail, and quote request_id to support.

import dialerbee

try:
    db.campaigns.resume(campaign_id)
except dialerbee.TierNotEnabledError:
    ...                                 # Tier-2 action not switched on for your account
except dialerbee.RateLimitError as e:
    print("retry after", e.retry_after)
except dialerbee.APIStatusError as e:
    print(e.status_code, e.code, e.request_id, [(x.pointer, x.code) for x in e.errors])
except dialerbee.APIConnectionError:
    ...                                 # no answer at all (network, DNS, TLS, timeout)
Class When
BadRequestError (400) invalid request; see errors
AuthenticationError (401) key missing, unknown, expired or revoked: every key problem answers invalid_api_key, so a key's state is never revealed
PermissionDeniedError (403) InsufficientScopeError, TierNotEnabledError, IP list, legal hold
NotFoundError (404) no such object for your account
ConflictError (409) IdempotencyConflictError, OutcomeUnknownError, RequestInProgressError, state conflicts
GoneError (410) for example a deleted recording
PreconditionFailedError (412) / PreconditionRequiredError (428) If-Match did not match / is missing
UnprocessableEntityError (422) refused by the platform (DNC, blocked country, refused URL)
RateLimitError (429) slow down; DailyCapReachedError for a Tier-2 daily cap
ServerError (5xx) ServiceUnavailableError for 503
ResponseValidationError a success answer did not match the contract

New error codes can appear inside v1; the SDK keeps them in code and picks the class by status.

Retries and idempotency

The SDK retries automatically (by default up to 2 times) only when it is safe:

Every write that needs an Idempotency-Key gets one generated for that call when you pass none. To also survive a crash of your own process, create the key yourself (for example dialerbee.new_idempotency_key()), store it with your record, and pass idempotency_key= on every attempt. Keys are scoped to your API client and kept for 24 hours.

OutcomeUnknownError means DialerBee could not confirm whether the write happened. Do not retry with a new key. Retry later with the same idempotency_key (it is on the exception) to get the settled answer, or contact support with request_id.

max_retries=0 turns retries off; db.with_options(max_retries=0, timeout=5) changes them for a few calls.

Tier 2 actions

Resuming a campaign, creating or moving callbacks and appointments, recording consent and sending SMS can reach or unlock real people. They need their own scope and an account switch that DialerBee keeps off unless it is agreed with you; until then they raise TierNotEnabledError. db.me.get().tier2 shows which are on. SMS sending is blocked for every account in API v1.

API v1 can check and add Do-Not-Call numbers but cannot remove them. Removing a number from the Do-Not-Call list stays a console action for an account administrator.

Configuration

import httpx
from dialerbee import DialerBee

db = DialerBee(
    api_key="dbk_live_...",
    timeout=30,                      # seconds, or an httpx.Timeout (default 60 s, 10 s connect)
    max_retries=2,
    http_client=httpx.Client(proxy="http://proxy.internal:3128"),  # optional; you close it
)

Async

import asyncio
from dialerbee import AsyncDialerBee

async def main() -> None:
    async with AsyncDialerBee() as db:
        page = await db.calls.list(limit=100)
        async for call in page:
            print(call.id)

asyncio.run(main())

Models

All request and response models are generated from the API contract (dialerbee.models). Response models keep fields that DialerBee adds later inside v1, so an older SDK keeps working. Request models refuse unknown fields and naive datetimes before anything is sent; timestamps go out in UTC.