dial_sdk

Submodules

Attributes

Classes

AudioCallConnected

AudioCallEnded

AudioDtmf

AudioDurationWarning

AudioMark

AudioMedia

Billing

BillingActivityItem

One activity-ledger item from GET /api/v1/billing/activity. type selects

BillingActivityPage

One page of activity, newest-first. has_more signals further pages.

BillingDeposit

BillingNumber

BillingPaymentMethod

BillingPricing

BillingSubscription

Call

CallConnected

CallStatus

Server-derived, client-facing call status (camelCase, as returned).

CallStatusChanged

dict() -> new empty dictionary

CallTranscribed

dict() -> new empty dictionary

CapabilityState

One capability's state.

Contact

One phone number the account's lines have talked to.

ContactsPage

One page of contacts, and whether older ones remain.

CreateGroupResult

What DialClient.create_group() returns.

DialClient

DialConfig

EventsConnection

A long-lived subscription to the account's event stream, consumed as an

Group

A group conversation — a conversation that isn't a phone number.

GroupAddFailure

One participant DialClient.create_group() could not add.

Interrupt

MakeCallParams

MediaItem

Raw bytes to upload as a media attachment; the MIME type is required.

Message

MessageMediaItem

A media attachment on a message, hosted by Dial.

MessagePoll

MessageStatusChanged

dict() -> new empty dictionary

MessageStatusChangedData

An advance on one of an outbound message's two independent axes.

NumberLookup

What a phone number can receive.

NumberStatusChanged

dict() -> new empty dictionary

NumberStatusChangedData

A change to one of a phone number's capabilities.

PhoneNumber

PingPong

Policy

An allow list for inbound calls.

PolicyEntry

PollInput

A poll to send instead of text (iMessage numbers only): 1-300 char title, 2-10 unique options.

PollOption

PollVote

PurchaseNumberParams

ReminderRequired

Response

ResponseRequired

SendMessageParams

TranscriptItem

TranscriptTurn

One uninterrupted stretch of speech by one party, placed in time.

TranscriptUpdate

TypingSession

A scoped typing indicator — see DialClient.typing().

Functions

parse_dial_audio_message(raw) → DialAudioMessage

Parse + validate a frame received from Dial. Raises ValidationError.

parse_dial_message(raw) → DialServerMessage

Parse + validate a frame received from Dial. Raises ValidationError.

serialize_server_audio_message(message) → str

Validate + serialize an outbound frame to a JSON string.

serialize_server_message(message) → str

Validate + serialize an outbound frame to a JSON string (omitting unset

verify_dial_signature(secret, header, data, *[, now, ...]) → bool

Verify a Dial X-Dial-Signature: t=<unix_seconds>,v1=<hex> header.

Package Contents

class dial_sdk.AudioCallConnected

Bases: pydantic.BaseModel

call_id: str
direction: Literal['inbound', 'outbound']
formats: AudioFormats
from_: str
instruction: str | None = None
language: str | None = None
model_config
reconnect: bool = False
to: str
type: Literal['call_connected']
class dial_sdk.AudioCallEnded

Bases: pydantic.BaseModel

reason: CallEndReason
type: Literal['call_ended']
class dial_sdk.AudioDtmf

Bases: pydantic.BaseModel

digit: str
type: Literal['dtmf']
class dial_sdk.AudioDurationWarning

Bases: pydantic.BaseModel

seconds_remaining: int
type: Literal['duration_warning']
class dial_sdk.AudioMark

Bases: pydantic.BaseModel

name: str
type: Literal['mark']
class dial_sdk.AudioMedia

Bases: pydantic.BaseModel

payload: str
seq: int | None = None
type: Literal['media']
class dial_sdk.Billing
classmethod from_api(d: dict) → Billing
balance_cents: int
deposits: list[BillingDeposit]
numbers: list[BillingNumber]
numbers_release_at: str | None
payment_methods: list[BillingPaymentMethod]
pricing: BillingPricing
subscription: BillingSubscription | None
class dial_sdk.BillingActivityItem

One activity-ledger item from GET /api/v1/billing/activity. type selects which fields are populated: - ‘usage’ → fare_name, number, billed_quantity, total_cents, attribution, phone_number_id, call_id, message_id - ‘credit’ → amount_cents, kind, invoice_id - ‘payment’ → amount_cents, invoice_id, reason

classmethod from_api(d: dict) → BillingActivityItem
amount_cents: int | None = None
attribution: str | None = None
billed_quantity: int | None = None
call_id: str | None = None
fare_name: str | None = None
invoice_id: str | None = None
kind: str | None = None
message_id: str | None = None
number: str | None = None
occurred_at: str
phone_number_id: str | None = None
reason: str | None = None
total_cents: int | None = None
type: str
class dial_sdk.BillingActivityPage

One page of activity, newest-first. has_more signals further pages.

classmethod from_api(d: dict) → BillingActivityPage
data: list[BillingActivityItem]
has_more: bool
class dial_sdk.BillingDeposit
classmethod from_api(d: dict) → BillingDeposit
amount_cents: int
created_at: str
invoice_id: str | None = None
kind: str
class dial_sdk.BillingNumber
classmethod from_api(d: dict) → BillingNumber
id: str
mode: str
nickname: str | None = None
number: str
class dial_sdk.BillingPaymentMethod
classmethod from_api(d: dict) → BillingPaymentMethod
brand: str
email: str | None
exp_month: int
exp_year: int
id: str
is_default: bool
last4: str
type: str
class dial_sdk.BillingPricing
classmethod from_api(d: dict) → BillingPricing
annual_cents: int
monthly_cents: int
class dial_sdk.BillingSubscription
classmethod from_api(d: dict) → BillingSubscription
cancel_at_period_end: bool
interval: str
period_end: str
period_start: str
quantity: int
class dial_sdk.Call
classmethod from_api(d: dict) → Call
blocked_by_policy_id: str | None = None
call_started_at: str | None
cancel_requested_at: str | None
created_at: str
direction: str
duration: int
failure_reason: str | None
from_: str
id: str
instruction: str | None
phone_number_id: str
queued_at: str | None
started_ringing_at: str | None
status: CallStatus
terminated_at: str | None
termination_type: str | None
to: str
transcript: str | None
transcript_turns: list[TranscriptTurn] | None
transfer_to: str | None
transferred_at: str | None
class dial_sdk.CallConnected

Bases: pydantic.BaseModel

call_id: str
direction: Literal['inbound', 'outbound']
from_: str
instruction: str | None = None
language: str | None = None
model_config
to: str
type: Literal['call_connected']
class dial_sdk.CallStatus

Bases: TypedDict

Server-derived, client-facing call status (camelCase, as returned).

Initialize self. See help(type(self)) for accurate signature.

cancelPending: bool
cancelRequested: bool
label: str
state: str
terminationType: str | None
class dial_sdk.CallStatusChanged

Bases: TypedDict

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object’s

(key, value) pairs

dict(iterable) -> new dictionary initialized as if via:

d = {} for k, v in iterable:

d[k] = v

dict(**kwargs) -> new dictionary initialized with the name=value pairs

in the keyword argument list. For example: dict(one=1, two=2)

Initialize self. See help(type(self)) for accurate signature.

createdAt: str
data: CallStatusChangedData
id: str
object: Literal['event']
relatedObject: RelatedObject | None
type: Literal['call.status_changed']
version: int
class dial_sdk.CallTranscribed

Bases: TypedDict

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object’s

(key, value) pairs

dict(iterable) -> new dictionary initialized as if via:

d = {} for k, v in iterable:

d[k] = v

dict(**kwargs) -> new dictionary initialized with the name=value pairs

in the keyword argument list. For example: dict(one=1, two=2)

Initialize self. See help(type(self)) for accurate signature.

createdAt: str
data: CallTranscribedData
id: str
object: Literal['event']
relatedObject: RelatedObject | None
type: Literal['call.transcribed']
version: int
class dial_sdk.CapabilityState

Bases: TypedDict

One capability’s state.

previousStatus, error and retryAvailableAt appear only on the asynchronous capabilities (call on an iMessage number, and whatsapp); a synchronous one has no transition to report and cannot fail.

Initialize self. See help(type(self)) for accurate signature.

error: str | None
previousStatus: str | None
retryAvailableAt: str | None
status: str
class dial_sdk.Contact

One phone number the account’s lines have talked to.

Derived from message and call history rather than stored: there is no address book to add anyone to, so a contact has no id — the number is its identity.

classmethod from_api(d: dict) → Contact
call_count: int
last_at: str
last_body: str
last_call_duration: int | None
last_direction: str
last_kind: str
last_media_count: int
last_redacted: bool
message_count: int
number: str
class dial_sdk.ContactsPage

One page of contacts, and whether older ones remain.

classmethod from_api(d: dict) → ContactsPage
contacts: list[Contact]
has_more: bool
class dial_sdk.CreateGroupResult

What DialClient.create_group() returns.

classmethod from_api(d: dict) → CreateGroupResult
group: Group
not_added: list[GroupAddFailure]
class dial_sdk.DialClient(config: dial_sdk.types.DialConfig)
async add_policy_entries(policy_id: str, entries: list[dict]) → dial_sdk.types.Policy

dial policy allow <id> <numbers…> — add callers ({“number”, “label”}) without resending the list.

async close()
async create_group(*, name: str, participants: list[str], from_number: str | None = None, from_number_id: str | None = None, channel: str | None = None) → dial_sdk.types.CreateGroupResult

dial group create — create a WhatsApp group from one of your lines, which becomes its admin.

from_number is the line’s id, its number in E.164, or a nickname; from_number_id is the legacy alternative. Provide exactly one (enforced by the server). name is the group’s subject (1–100 characters); participants are 1–256 E.164 numbers, deduplicated by the server — the creating line’s own number is refused. channel is the channel to create the group on: only "whatsapp" today, and the default when omitted.

Returns once the group exists; its id works as group_id on send_message() right away. A participant who could not be added comes back in not_added rather than failing the call — privacy_restricted means their settings only let contacts add them.

A 504 means the group was not confirmed in time and may still exist: check list_groups() before trying again, or you may create it twice.

async create_policy(*, name: str, scope: str, number_ids: list[str] | None = None, enabled: bool | None = None, entries: list[dict] | None = None) → dial_sdk.types.Policy

dial policy create — create an allow list for inbound calls.

scope “account” covers every number (at most one per account); scope “numbers” covers number_ids and replaces the account-wide list on them. entries are {“number”: E.164, “label”: optional}. An enabled list with no entries declines every inbound call on the numbers it covers.

async delete_policy(policy_id: str) → None

dial policy delete <id> — numbers it covered fall back to the account-wide list, if any.

async get_billing() → dial_sdk.types.Billing

dial billing — wallet balance, subscription, per-number mode, recent credits, payment methods.

async get_call(call_id: str) → dial_sdk.types.Call

dial call get <id> — fetch a single call by id.

async get_policy(policy_id: str) → dial_sdk.types.Policy

dial policy get <id> — one allow list, with its callers.

async list_all_contacts(page_size: int = 1000, *, number_id: str | None = None) → list[dial_sdk.types.Contact]

Every contact, following the cursor until the API reports no more.

“Who have I talked to” is not answered by a first page, so this is usually the method you want. number_id narrows it to one line’s contacts. The walk is bounded (25 pages) so a has_more that never goes false cannot loop forever.

async list_billing_activity(*, filter: str = 'all', limit: int | None = None, starting_after: str | None = None) → dial_sdk.types.BillingActivityPage

One page of the activity ledger (usage + credits + subscription payments), newest-first. Stripe-style cursor: pass starting_after set to the occurred_at of the last item to fetch the next page; has_more signals more.

async list_calls(*, number_id: str | None = None, direction: str | None = None, since: str | None = None, contact: str | None = None) → list[dial_sdk.types.Call]

dial call list — list calls, optionally filtered.

contact reads one person’s call history: calls exchanged with that number, both directions, across every line on the account.

async list_contacts(*, number_id: str | None = None, limit: int | None = None, starting_after: str | None = None) → dial_sdk.types.ContactsPage

dial contacts — one page of the numbers your lines have talked to.

Newest activity first. A contact is derived from message and call history, not stored: a number appears the moment it first exchanges anything with one of your lines, and there is no address book to add anyone to. Counts span every line on the account, so one person who reached two of your numbers is one contact — pass number_id for one line’s contacts and counts instead (an id not on your account yields an empty page). Groups are not contacts — see list_groups().

starting_after is the last_at of the last contact from the previous page; only contacts with strictly older activity are returned. Most accounts come back in one page — list_all_contacts() follows the cursor when they don’t.

async list_groups() → list[dial_sdk.types.Group]

dial group list — the group conversations your lines are in.

A group is a conversation that isn’t a phone number, so it carries an id of its own: pass it as group_id to send_message() or list_messages(). Groups exist on WhatsApp lines and iMessage numbers.

A group’s name may be None — it is read live from the line holding the conversation, so a line that cannot answer in time yields a null name rather than hiding the group. There is no join event: list again to see a group your line was just added to.

async list_messages(*, number_id: str | None = None, direction: str | None = None, since: str | None = None, group_id: str | None = None, contact: str | None = None, search: str | None = None) → list[dial_sdk.types.Message]

dial message list — list messages, optionally filtered.

Unfiltered this is the newest 100 messages ACROSS every conversation, so one thread’s history is capped by how busy the others are.

contact reads one person’s conversation as itself: messages exchanged with that number, both directions, across every line on the account. Group messages are never included — a group message is addressed to the group, and matching it against the participant who sent it would file their whole group into a private thread. Use group_id for those.

search matches the message body case-insensitively, anywhere in the text, over your whole history, and then returns the 100 most recent matches. Substring rather than full-text: no stemming and no word boundaries, so voice matches “invoiced”.

group_id restricts the list to one group conversation and combines with the other filters. A group id that isn’t on your account yields an empty list rather than an error.

async list_numbers() → list[dial_sdk.types.PhoneNumber]

dial number list — list the account’s phone numbers.

async list_policies() → list[dial_sdk.types.Policy]

dial policy list — the account’s allow lists (entry_count, no entries).

async lookup_number(number: str) → dial_sdk.types.NumberLookup

dial lookup — what channels a phone number can receive on.

Works on ANY number in the world: it need not be one of yours, and you need not have messaged it before. Use it to pick a channel before spending a send.

A lookup Dial could not complete RAISES (502) rather than returning a negative verdict, so a False always means the number genuinely is not reachable on that channel — never that we could not find out. The two are worth treating differently: only one is worth retrying.

supports["whatsapp"] is None when your account has no WhatsApp number of its own — a third value with its own single meaning, and never a failure either. See NumberLookup.

async make_call(params: dial_sdk.types.MakeCallParams) → dial_sdk.types.Call

dial call — place an outbound AI voice call.

new_events_connection() → dial_sdk.events.EventsConnection

Open a long-lived connection to the account’s event stream.

SDK equivalent of dial wait-for: rather than returning a single event, it yields every message.received / call.status_changed / call.ended / call.transcribed event on the account channel. The PubNub token is re-minted automatically before it expires. Every event shares one envelope; read values from event["data"]. No I/O happens until the connection is entered:

async with client.new_events_connection() as conn:
    async for event in conn:
        if event["type"] == "message.received":
            print(event["data"]["from"], event["data"]["body"])
async purchase_number(params: dial_sdk.types.PurchaseNumberParams) → dial_sdk.types.PhoneNumber

dial number purchase — provision a new phone number.

async remove_policy_entries(policy_id: str, numbers: list[str]) → dial_sdk.types.Policy

dial policy disallow <id> <numbers…> — remove callers; ones not on the list are ignored.

async reply_to_message(message_id: str, *, body: str | None = None, reaction: str | None = None) → dial_sdk.types.Message

dial message reply — reply or react to an existing message.

Exactly one of body (threaded reply) or reaction (a reaction name — love/like/dislike/laugh/emphasize/question — or a single emoji) is required. The server derives the sender and recipient from the target message, so there is no to/from_number_id.

async send_message(params: dial_sdk.types.SendMessageParams) → dial_sdk.types.Message

dial message — send a message, optionally with media attachments (MMS).

async set_inbound_instruction(number_id: str, inbound_instruction: str) → dial_sdk.types.PhoneNumber

Deprecated: use set_number_properties() instead.

async set_number_properties(number_id: str, *, inbound_instruction: str | None = None, inbound_voice_gender: str | None = _UNSET, inbound_language: str | None = _UNSET, nickname: str | None = _UNSET, max_call_duration_seconds: int | None = None, calling_enabled: bool | None = None, forward_to: str | None = _UNSET, channel: str | None = None, name: str | None = None, first_name: str | None = _UNSET, last_name: str | None = _UNSET, avatar: str | Path | MediaItem | None = None, whatsapp_name: str | None = _UNSET, whatsapp_avatar: str | Path | MediaItem | None = None) → dial_sdk.types.PhoneNumber

dial number set — update a number’s properties (any subset; at least one).

nickname=None or nickname="" clears the nickname; omit the argument to leave it unchanged. inbound_voice_gender=None clears the inbound voice (reverts to the default, female); pass “male”/”female” to set it. inbound_language=None clears the inbound language (reverts to per-call detection from the caller’s country prefix); pass a BCP-47 tag (e.g. “es-ES”) to pin inbound calls to one language.

channel with name and/or avatar sets one display identity on one or both channels: "imessage", "whatsapp", or "both". Prefer "both" when the number should look the same everywhere — it is a single call. Each channel succeeds or fails on its own: a value one channel rejects fails only that channel, the rest is saved, and the returned number’s profile_results gives each targeted channel’s outcome ("updated" or "failed"). The call raises only when every targeted channel failed, and then nothing changed. WhatsApp stores name verbatim (1–25 chars); iMessage has no single-name field, so it is split on the first space — "Ana Lee" becomes first name "Ana", last name "Lee". channel cannot be combined with the per-channel arguments below.

first_name / last_name set the iMessage display identity on its own — the name shown beside the number’s messages in recipients’ Messages apps (max 30 chars; None or "" clears; iMessage numbers only — rejected with 400 otherwise). avatar sets the identity photo: an http(s) URL string (downloaded server-side), a local image Path, or a MediaItem of raw bytes (both uploaded as multipart). Without channel that is the iMessage photo: jpeg/png/gif/webp, max 5 MB. With channel it goes to the channel(s) named there. WhatsApp’s rules are stricter — square jpeg or png, 192×192 to 640×640, not resized, with no transparent pixels — and a photo that misses them fails only the WhatsApp channel ("failed" in profile_results) while iMessage still takes it. Replace-only — a photo can be replaced but not removed.

calling_enabled switches calling on or off for the number, in both directions: False stops inbound calls from being connected (the caller is never answered) and makes make_call() from this number raise calling_disabled (409). Messaging is unaffected. It takes effect on the next call — a call already in progress is not ended. Omit the argument (or pass None) to leave it unchanged.

forward_to forwards inbound calls to this phone number (E.164) instead of having the AI voice agent answer; the call ends if that phone is busy or doesn’t answer. forward_to=None stops forwarding; omit the argument to leave it unchanged. Rejected (400) when it isn’t valid E.164 or is the number’s own number. Has no effect while calling is switched off (calling_enabled=False).

async start_typing(*, to_number: str, from_number: str, channel: str | None = None) → None

dial typing start — show a typing indicator to the recipient.

iMessage numbers display it; standard (SMS) numbers have no typing concept and silently ignore it, so calling this unconditionally is safe. Fire-and-forget and free. Delivering a message or reaction clears the indicator natively on the recipient’s device — start again after a send to keep composing, and pair with stop_typing when you stop without sending (or use typing(), whose session renews it after each send and stops it on exit). from_number is a flexible ref: a phone-number id, one of your numbers in E.164, or a nickname.

async stop_call(call_id: str) → dial_sdk.types.Call

dial call stop <id> — end a call that hasn’t finished.

A queued or ringing call is cancelled before it connects; an in-progress one is hung up. Returns once the stop is accepted, with status.cancelPending set — the call reaches Terminated a moment later, so wait for call.ended to confirm. Raises (400) if the call had already ended.

async stop_typing(*, to_number: str, from_number: str, channel: str | None = None) → None

dial typing stop — clear a typing indicator shown with start_typing.

Pass the same channel start_typing was given.

typing(*, to_number: str, from_number: str, channel: str | None = None) → TypingSession

Scope a typing indicator to a block: starts on enter, stops on exit.

async with dial.typing(to_number="+1…", from_number="Support line") as session:
    await session.send_message(body="…")  # to/from prefilled

Delivering a message clears the indicator natively on the recipient’s device, so the session renews it after each send_message — the indicator persists until __aexit__, the only real stop. Teardown is best-effort: a failure to clear the indicator never masks an exception raised inside the block (and is swallowed on a clean exit too). There is no keep-alive; the recipient’s device may drop a stale indicator during a very long pause.

async update_policy(policy_id: str, *, name: str | None = None, scope: str | None = None, number_ids: list[str] | None = None, enabled: bool | None = None, entries: list[dict] | None = None) → dial_sdk.types.Policy

dial policy update <id> — change any subset of fields; entries replaces the whole list.

class dial_sdk.DialConfig
api_key: str
base_url: str | None = None
user_agent: str | None = None
class dial_sdk.EventsConnection(mint: MintGrant)

A long-lived subscription to the account’s event stream, consumed as an async iterator of event dicts.

This is the SDK’s idiom for what the CLI does one-shot with dial wait-for: instead of returning a single event, it yields every message.received / call.status_changed / call.ended / call.transcribed / number.status_changed event on the account channel until the consumer stops iterating or the connection is closed. The PubNub token is silently re-minted before it expires, so a connection can stay open indefinitely. PubNub specifics (subscribe key, channel, token, TTL) are never surfaced to the caller.

Usage:

async with client.new_events_connection() as conn:
    async for event in conn:
        handle(event)
async close() → None

Tear down the subscription and end iteration. Idempotent.

async open() → EventsConnection

Mint the first token and establish the subscription. Idempotent.

class dial_sdk.Group

A group conversation — a conversation that isn’t a phone number.

Pass id as group_id when sending or listing messages.

classmethod from_api(d: dict) → Group
channel: Literal['whatsapp', 'imessage']
created_at: str
id: str
name: str | None
class dial_sdk.GroupAddFailure

One participant DialClient.create_group() could not add.

classmethod from_api(d: dict) → GroupAddFailure
number: str | None
reason: Literal['privacy_restricted', 'not_added']
class dial_sdk.Interrupt

Bases: pydantic.BaseModel

content: str
content_complete: bool
end_call: bool | None = None
type: Literal['interrupt']
class dial_sdk.MakeCallParams
from_number: str | None = None
from_number_id: str | None = None
idempotency_key: str | None = None
language: str | None = None
max_call_duration_seconds: int | None = None
outbound_instruction: str = ''
to: str
transfer_to: str | None = None
voice_gender: str | None = None
class dial_sdk.MediaItem

Raw bytes to upload as a media attachment; the MIME type is required.

content_type: str
data: bytes
filename: str = 'media'
class dial_sdk.Message
classmethod from_api(d: dict) → Message
body: str
channel: str
created_at: str
delivery_error: str | None = None
delivery_state: str | None = None
direction: str
from_: str
group_id: str | None = None
id: str
media: list[MessageMediaItem] = []
phone_number_id: str
poll: MessagePoll | None = None
poll_vote: PollVote | None = None
reaction: str | None = None
read_at: str | None = None
read_state: str | None = None
reply_to_id: str | None = None
status: str
status_error: str | None = None
to: str | None
class dial_sdk.MessageMediaItem

A media attachment on a message, hosted by Dial.

classmethod from_api(d: dict) → MessageMediaItem
content_type: str
id: str
original_url: str | None
url: str
class dial_sdk.MessagePoll
classmethod from_api(d: dict) → MessagePoll
options: list[PollOption] = []
class dial_sdk.MessageStatusChanged

Bases: TypedDict

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object’s

(key, value) pairs

dict(iterable) -> new dictionary initialized as if via:

d = {} for k, v in iterable:

d[k] = v

dict(**kwargs) -> new dictionary initialized with the name=value pairs

in the keyword argument list. For example: dict(one=1, two=2)

Initialize self. See help(type(self)) for accurate signature.

createdAt: str
data: MessageStatusChangedData
id: str
object: Literal['event']
relatedObject: RelatedObject | None
type: Literal['message.status_changed']
version: int
class dial_sdk.MessageStatusChangedData

Bases: TypedDict

An advance on one of an outbound message’s two independent axes.

Delivery and reads are separate facts — not every rail reports both — so the payload carries the current value of BOTH axes plus changed naming the one that just moved. Each event is therefore a complete snapshot.

Initialize self. See help(type(self)) for accurate signature.

changed: str
channel: str
deliveryError: str | None
deliveryState: str
from_: str
messageId: str
phoneNumberId: str
readState: str
to: str
class dial_sdk.NumberLookup

What a phone number can receive.

supports describes the number that was asked about, NOT one of your own lines. It is a point-in-time answer rather than a property of the number: someone who changes device or turns a service off stops being reachable, so treat a True as a strong signal for picking a channel, not a promise that a later send is delivered.

classmethod from_api(d: dict) → NumberLookup
number: str
supports: dict[str, bool]
class dial_sdk.NumberStatusChanged

Bases: TypedDict

dict() -> new empty dictionary dict(mapping) -> new dictionary initialized from a mapping object’s

(key, value) pairs

dict(iterable) -> new dictionary initialized as if via:

d = {} for k, v in iterable:

d[k] = v

dict(**kwargs) -> new dictionary initialized with the name=value pairs

in the keyword argument list. For example: dict(one=1, two=2)

Initialize self. See help(type(self)) for accurate signature.

createdAt: str
data: NumberStatusChangedData
id: str
object: Literal['event']
relatedObject: RelatedObject | None
type: Literal['number.status_changed']
version: int
class dial_sdk.NumberStatusChangedData

Bases: TypedDict

A change to one of a phone number’s capabilities.

Wait on this instead of polling a number until it is usable.

status folds the capabilities: unsettled while any is still working, ready when all are ready, degraded when everything has finished trying and something failed. ready and degraded are both terminal, so waiting on either always resolves — degraded exists because WhatsApp can be declined outright while the number still works for calls and messages.

Payload version 2. Version 1 was flat (setupStatus/setupError and a capabilities string list) and had nowhere to put a WhatsApp failure or its cooldown.

Initialize self. See help(type(self)) for accurate signature.

capabilities: dict[str, CapabilityState]
changed: list[str]
number: str
phoneNumberId: str
previousStatus: str | None
status: str
class dial_sdk.PhoneNumber
classmethod from_api(d: dict) → PhoneNumber
account_id: str
avatar_url: str | None = None
calling_enabled: bool
capabilities: list[str]
country: str
created_at: str
first_name: str | None = None
forward_to: str | None = None
id: str
inbound_call_policy: dict | None = None
inbound_instruction: str | None
inbound_language: str | None
inbound_voice_gender: str | None
last_name: str | None = None
max_call_duration_seconds: int | None
nickname: str | None
number: str
profile_results: dict | None = None
replaced_at: str | None = None
replaced_by: dict | None = None
replaces: list[dict] = []
setup_error: str | None
setup_status: str
whatsapp: dict | None = None
whatsapp_avatar_url: str | None = None
whatsapp_name: str | None = None
class dial_sdk.PingPong

Bases: pydantic.BaseModel

timestamp: int
type: Literal['ping_pong']
class dial_sdk.Policy

An allow list for inbound calls.

Only callers on it ring through on the numbers it covers; every other inbound call is declined before it connects. scope “account” covers every number without a list of its own; scope “numbers” covers number_ids and replaces the account-wide list on them. A disabled policy counts as if it weren’t there.

classmethod from_api(d: dict) → Policy
applies_to: str
blocked_calls_last_7_days: int
created_at: str
enabled: bool
entries: list[PolicyEntry] | None = None
entry_count: int
id: str
name: str
number_ids: list[str]
scope: str
type: str
updated_at: str
class dial_sdk.PolicyEntry
classmethod from_api(d: dict) → PolicyEntry
label: str | None
number: str
class dial_sdk.PollInput

A poll to send instead of text (iMessage numbers only): 1-300 char title, 2-10 unique options.

options: list[str]
title: str
class dial_sdk.PollOption
classmethod from_api(d: dict) → PollOption
id: str
text: str
votes: list[str] = []
class dial_sdk.PollVote
classmethod from_api(d: dict) → PollVote
option_id: str
selected: bool
class dial_sdk.PurchaseNumberParams
area_code: str | None = None
calling_enabled: bool | None = None
inbound_instruction: str
inbound_language: str | None = None
inbound_voice_gender: str | None = None
include_imessage: bool = False
class dial_sdk.ReminderRequired

Bases: pydantic.BaseModel

response_id: int
transcript: list[TranscriptItem]
type: Literal['reminder_required']
class dial_sdk.Response

Bases: pydantic.BaseModel

content: str
content_complete: bool
end_call: bool | None = None
response_id: int
type: Literal['response']
class dial_sdk.ResponseRequired

Bases: pydantic.BaseModel

response_id: int
transcript: list[TranscriptItem]
type: Literal['response_required']
class dial_sdk.SendMessageParams
body: str = ''
channel: str | None = None
force_audio_file: bool = False
from_number: str | None = None
from_number_id: str | None = None
group_id: str | None = None
media: list[SendMessageMedia] = []
poll: PollInput | None = None
to: str = ''
class dial_sdk.TranscriptItem

Bases: pydantic.BaseModel

content: str
role: Role
class dial_sdk.TranscriptTurn

One uninterrupted stretch of speech by one party, placed in time.

Offsets count milliseconds from the start of the call’s audio, so the pause before a turn is its start_ms minus the previous turn’s end_ms.

The offsets are approximate: they come from per-word timings the voice runtime does not guarantee to be exact. Ample for spotting a long silence, which is what they are for; not a basis for splitting tenths of a second.

classmethod from_api(d: dict) → TranscriptTurn
end_ms: int
speaker: str
start_ms: int
text: str
class dial_sdk.TranscriptUpdate

Bases: pydantic.BaseModel

transcript: list[TranscriptItem]
type: Literal['transcript_update']
class dial_sdk.TypingSession(client: DialClient, *, to_number: str, from_number: str, channel: str | None = None)

A scoped typing indicator — see DialClient.typing().

async send_message(**kwargs) → dial_sdk.types.Message

Send a message in this conversation — to and the from-number ref are prefilled.

Accepts the same keyword fields as SendMessageParams minus the addressing (to/from_number/from_number_id). The delivered message clears the indicator natively on the recipient’s device, so the session renews it right after the send — the indicator persists until __aexit__.

The session’s channel is prefilled too, so a session opened on WhatsApp does not show its indicator there and then send on the line’s default rail — a different conversation to the recipient. An explicit channel keyword still wins.

dial_sdk.parse_dial_audio_message(raw: str | bytes | dict) → DialAudioMessage

Parse + validate a frame received from Dial. Raises ValidationError.

dial_sdk.parse_dial_message(raw: str | bytes | dict) → DialServerMessage

Parse + validate a frame received from Dial. Raises ValidationError.

dial_sdk.serialize_server_audio_message(message: ServerAudioMessage) → str

Validate + serialize an outbound frame to a JSON string.

dial_sdk.serialize_server_message(message: ServerDialMessage) → str

Validate + serialize an outbound frame to a JSON string (omitting unset end_call).

dial_sdk.verify_dial_signature(secret: str, header: str, data: str, *, now: int | None = None, tolerance_seconds: int = 300) → bool

Verify a Dial X-Dial-Signature: t=<unix_seconds>,v1=<hex> header.

The signature is HMAC-SHA256(secret, "<t>.<data>"), where data is the value the signature covers — the call_id for the self-hosted WebSocket protocol, or the raw request body for a webhook delivery. Returns False (never raises) on a malformed header, a stale/future timestamp, or a mismatch. Matches Dial’s server-side signer byte-for-byte.

dial_sdk.CallEvent
dial_sdk.DialEvent
dial_sdk.MessageEvent
dial_sdk.SendMessageMedia