# Dialpad

> Send SMS and make voice calls via Dialpad API using task-focused wrappers backed by an OpenAPI-generated CLI.

- Skill: `kesslerio/dialpad` (Agent Skill, multi-file: 10 files)
- Install (CLI): `npx skillmds@latest add kesslerio/dialpad`
- Raw SKILL.md: https://api.skillmd.com/api/skills/kesslerio/dialpad/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Integrations & APIs
- Author: kesslerio (https://skillmd.com/u/kesslerio)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/kesslerio/dialpad

---


# Dialpad Skill

Send SMS and make voice calls via the Dialpad API.

## When to Use

**Native tools:** `submit_draft` (registered by this skill's `dialpad-draft-callback` plugin). **Owned entrypoints:** `list_sms_thread.py`, `list_sms_inbox.py`, `list_call_history.py`, `lookup_contact.py`, `poll_voicemails.py`.

Use this skill to:
- Send SMS messages (individual or batch)
- Make voice calls (with TTS or custom voices)
- Manage contacts and organization settings
- Inspect SMS and call history through operator tooling when needed

## Available Phone Numbers

| Number | Purpose | Format |
|--------|---------|--------|
| (415) 555-0140 | Sales Team | Default for sales context |
| (415) 555-0100 | Work/Personal | Default for work context |
| (415) 555-0141 | Support SMS Only | SMS only (no voice) |

## Quick Start

**Send SMS (explicit sender recommended):**
```bash
bin/send_sms.py --to "+14155551234" --from "+14155550140" --message 'Hello from OpenClaw!'
```

**Send an operator-approved SMS and close a shown approval draft (agent direct path):**
```bash
bin/send_sms.py --to "+14155551234" --from "+14155550140" --message 'Exact approved text' --resolve-draft-id smsdraft_abc123 --approval-actor-id "telegram-user-123" --approval-actor-username "operator" --json
```

**Create/approve an SMS draft (human approval path):**
```bash
bin/create_sms_draft.py --thread-key "manual:thread" --to "+14155551234" --from "+14155550140" --message 'Exact draft text' --json
bin/approve_sms_draft.py smsdraft_abc123 --actor-id "telegram-user-123" --actor-username "operator" --approval-token "$DIALPAD_SMS_APPROVAL_TOKEN" --json
```

**Group Intro (mirrored fallback):**
```bash
bin/send_group_intro.py --prospect "+14155550111" --reference "+14155559999" --confirm-share --from "+14155550100"
```

**Make Call (TTS):**
```bash
bin/make_call.py --to "+14155551234" --text "This is a call from the agent."
```

**List Recent Calls:**
```bash
bin/list_calls.py --today --limit 20
bin/list_calls.py --hours 6 --missed --json
```

**Get Call Transcript:**
```bash
bin/get_call_transcript.py --call-id "call_123" --json
bin/get_call_transcript.py --last --with "+14155551234" --json
```

**Check SMS Thread History:**
```bash
bin/list_sms_thread.py --phone "+14155551234" --json
bin/list_sms_inbox.py --limit 20 --json
```

When `DIALPAD_LOG_URL` is configured, SMS thread/inbox reads and call-list
reads use the authenticated theshop interaction log by default. Use
`bin/list_calls.py --live` only for an explicit live Dialpad query and
`--local` for the legacy local calls database. After a successful
`bin/send_sms.py`, the exact outbound observation is recorded through the
shared log or queued in the record-only `DIALPAD_LOG_OUTBOX`; replaying that
outbox never sends SMS or places calls.

**Sync Direct Dialpad SMS Sends:**
```bash
bin/sync_sms_export.py --start-date 2026-05-13 --end-date 2026-05-13 --json
```

**Create Contact:**
```bash
bin/create_contact.py --first-name "Jane" --last-name "Doe" --phone "+14155550123" --email "jane@example.com"
```

**Update Contact:**
```bash
bin/update_contact.py --id "contact_123" --phone "+14155550123" --job-title "VP"
```

## Key Rules

1. **Destination policy:** Outbound SMS and calls support NANP (`+1`) destinations only, including all NANP territories. Non-NANP international destinations are rejected before any Dialpad API request. Always use E.164 format (e.g., `+14155551234`); SMS country inference normalizes ten-digit NANP input to explicit `+1` E.164. The generated `message schedules.send_now` command is disabled because stored recipients cannot be validated locally; schedule updates must repeat phone recipients through `--data` as a JSON array; meeting updates must set `call_out` through `--data` as a JSON boolean; and generated meeting create/update commands cannot enable call-out.
2. **Escaping:** Use single quotes for inline `--message` values containing `$` to prevent shell expansion (e.g., `'Price is $10'`).
3. **Safer message input:** Prefer `--message-file` or `--message-stdin` for pricing text, multi-line copy, or anything shell-sensitive.
4. **Supported agent interface:** use `bin/*.py` wrappers for normal work. They are the stable command contract for agents.
5. **Operator-only surfaces:** `generated/dialpad` and `scripts/*` are for manual troubleshooting, storage inspection, or operational maintenance, not normal agent task execution.
6. **Auth canonical source:** `DIALPAD_API_KEY` is canonical. `DIALPAD_TOKEN` is only needed for manual generated CLI troubleshooting (bridge command in Setup below).
7. **SMS sender safety:** `--from` and `--profile work|sales` are supported. Prefer explicit `--from` for deterministic routing.
   - `--profile` maps to configured env vars:
     - work: `DIALPAD_PROFILE_WORK_FROM`
     - sales: `DIALPAD_PROFILE_SALES_FROM`
   - default fallback order: `DIALPAD_DEFAULT_FROM_NUMBER`, then `DIALPAD_DEFAULT_PROFILE`
   - `--allow-profile-mismatch` permits explicit/profile mismatches when intentional
   - `--dry-run` prints sender resolution and the exact message/request preview without an API call
8. **Group intro:** `bin/send_group_intro.py` mirrors intro messages as two one-to-one SMS sends (`mirrored_fallback`) because true group threads are unsupported via this wrapper.
9. **Call history:** `bin/list_calls.py` is the supported call-history command for agents (supporting `--today`, `--hours`, `--missed`, and `--local` for offline store queries). `bin/list_call_history.py` provides dedicated read-only local store queries with `--phone`, `--direction`, `--min-duration`, and `--transcript-only` filters. Use `--json` when downstream automation needs a deterministic response envelope.
10. **Call transcripts:** `bin/get_call_transcript.py` is the supported transcript retrieval command for agents. It supports `--local` to retrieve transcripts deterministically from the local SQLite store without hitting the live Dialpad API. When hitting the live API, it uses Dialpad's plural transcript endpoints (`GET /api/v2/transcripts/{call_id}` and optional review URL `GET /api/v2/transcripts/{call_id}/url`); singular `/api/v2/transcript/{id}` is not the supported endpoint. It is transcript-only; AI recap, CRM enrichment, and follow-up drafting are intentionally separate work.
11. **Call & SMS local stores:** Incoming webhooks persist events in local append-only SQLite databases (`DIALPAD_SMS_DB` defaulting to `/home/art/niemand/logs/sms.db`, and `DIALPAD_CALLS_DB` defaulting to `/home/art/niemand/logs/calls.db`). Before saying a contact was not messaged or called, inspect local history first. For SMS, run `bin/list_sms_thread.py --phone PHONE --json` and check `has_outbound` / `outbound_count`. For calls, run `bin/list_call_history.py --phone PHONE --json`.
12. **SMS export sync:** `bin/sync_sms_export.py` imports Dialpad Stats text export metadata into local SQLite for direct Dialpad UI sends and other out-of-band messages. It skips existing message IDs to preserve webhook-captured text.
13. **Create/Update Contact Behavior:** `bin/create_contact.py` upserts shared/local contacts by phone/email match (or forces create with `--allow-duplicate`). `bin/update_contact.py` updates by `--id` with partial fields.
14. **Current-turn verification:** "Already sent" and "Already updated" are only valid after a fresh current-turn tool result, not from stale session memory. If the current turn has not verified the action yet, say that plainly and run the tool now.
15. **SMS receipt ledger:** Successful sends through `bin/send_sms.py`, both legs of `bin/send_group_intro.py`, and approval-lane sends append authoritative JSONL receipt evidence to `DIALPAD_SMS_RECEIPT_LEDGER`, defaulting to `/data/.openclaw/state/dialpad/sms-receipts.jsonl`. Failed sends and dry runs do not write receipts.
16. **Receipt phrasing:** When confirming an SMS send to an operator, always include `To: <number>` in the receipt text so the downstream guard can bind the claim to the ledger recipient.
17. **Delivery-status webhooks:** `bin/create_sms_webhook.py create` provisions the SMS subscription with `status: true`. Authenticated sparse delivery events update only the matching `sms.db` row by provider message ID and never enter inbound dedupe, OpenClaw hooks, Telegram, or approval-draft fan-out. Receipt events require a parseable provider `event_timestamp`; an empty or `accepted` result is required alongside `delivered`, while unknown, stale, and contradictory states fail closed.
18. **Identity guardrail:** For first-contact work, soft signals like first name, area code, industry, or job title are not enough to merge or update a contact. Keep uncertain identity `draft-only` for **autonomous** sends and contact mutation — let the CRM layer prove the match before auto-sending or mutating anything. However, explicit operator approval (rule 20) lifts the draft-only send restriction: the whole point of the approval path is that the human makes the identity judgment call. Do not refuse to send a draft that an operator has explicitly approved in the current turn.
19. **Inbound context guardrail:** Webhook `inboundContext` briefs explain identity evidence, recent SMS/call continuity, source status, and draft basis. Low-confidence Sales SMS and missed calls may get generic approval drafts, but not personalized claims. Known contacts only get context-aware, CRM-aware, meeting-aware, or availability-aware approval drafts when identity confidence is high and relevant continuity is no older than 14 days; stale or degraded context stays brief-only. Calendar lookup is only for obvious meeting logistics and demo-prospect availability requests, not every inbound SMS. If availability cannot be verified, do not turn the CRM generic fallback into a scheduling reply. Demo-context missed calls may include operator-only prior-comms provenance from SMS/Gmail counts; raw comms bodies must not be copied into customer-facing draft text. If a model is used for final wording, it must receive compact tool-call facts plus the deterministic fallback and fail closed to that fallback on unsafe or unsupported output.
20. **Inbound automation guardrail:** Dialpad inbound hooks may create SMS approval drafts, but they must not send customer SMS directly. Use `bin/approve_sms_draft.py` only with a real human actor id and an operator-only `DIALPAD_SMS_APPROVAL_TOKEN`; agent/bot actors are rejected by the trusted approval ledger.
21. **Operator-approved agent sends:** Agents may send SMS with `bin/send_sms.py` after explicit current-turn operator approval. When resolving a shown approval draft, pass `--resolve-draft-id` plus `--approval-actor-id`/`--approval-actor-username` so the draft closes as an audited `agent_direct_send`; that actor context is agent-asserted unless it came through a trusted Telegram/shell approval surface. Risky drafts still require the existing two-step approval state before `--confirm-risk` can send.
22. **Opt-out guardrail:** Explicit opt-out language is a hard stop. Do not create override drafts or send follow-ups on those threads unless a human operator handles the conversation outside automation.
23. **Operator notification ownership:** One inbound SMS or missed call should create one operator-visible Telegram message per target by default. When the local Dialpad Telegram approval card and OpenClaw hook route point at the same Telegram destination, the local card owns visibility and the hook is context-only with `deliver=false`. Use `DIALPAD_ALLOW_DUPLICATE_OPERATOR_DELIVERY=1` only for intentional same-target fanout.
24. **Generated CLI command structure & safe discovery:** The underlying generated CLI uses `<resource> <resource>.<method>` naming (e.g. `dialpad sms sms.send`, `dialpad users users.get`, `dialpad call call.create_call`). Avoid probing guessed subcommands such as `users list` (which does not exist in this generated CLI). When inspecting commands, use `generated/dialpad <resource> --help` or rely on `bin/*.py` wrappers.
25. **Send SMS managed environment & direct API fallback:** `bin/send_sms.py` executes generated Dialpad CLI calls through a managed environment (automatically discovering `uv` across PATH and standard locations like `~/.cargo/bin/uv` with `click` and `requests`). If the generated CLI or its Python runtime dependencies are missing, `bin/send_sms.py` automatically falls back to direct Dialpad API execution, ensuring approved outbound SMS messages never remain unsent due to environment gaps.

## Reference Documentation

- **`references/api-reference.md`** — Wrapper behavior, operator CLI reference, Webhooks
- **`references/openclaw-integration.md`** — End-to-end setup guide for wiring this repo's webhook server to OpenClaw with human approval defaults
- **`references/sms-storage.md`** — SQLite commands, FTS5 search, legacy storage
- **`references/voice-options.md`** — List of available TTS voices (Budget & Premium)
- **`references/architecture.md`** — System architecture, wrappers, and CLI generation
- **`THEORY.MD`** — Design rationale: shell-corruption invariant, 401 classification, inbound-policy-once model

## Operational Tools

Use these only for manual operator workflows, storage inspection, and maintenance:

```bash
python3 scripts/sms_sqlite.py list
python3 scripts/webhook_server.py
```

## Setup

**Required environment variable:**
```bash
export DIALPAD_API_KEY="your_key"
```

**Operator auth bridge for manual generated CLI troubleshooting:**
```bash
export DIALPAD_TOKEN="${DIALPAD_TOKEN:-$DIALPAD_API_KEY}"
```

**Optional:**
```bash
export ELEVENLABS_API_KEY="your_key"
export DIALPAD_USER_MAP='{"+14155550100": "5765607478525952"}'
export DIALPAD_PROFILE_WORK_FROM="+14155550100"
export DIALPAD_PROFILE_SALES_FROM="+14155550140"
export DIALPAD_DEFAULT_PROFILE="work"
export DIALPAD_DEFAULT_FROM_NUMBER="+14155550140"
export DIALPAD_SMS_RECEIPT_LEDGER="/data/.openclaw/state/dialpad/sms-receipts.jsonl"
export DIALPAD_DRAFT_CALLBACK_URL="http://host.docker.internal:8888/internal/draft-callback"
```

For merged agent-draft flow, keep `DIALPAD_DRAFT_CALLBACK_URL` aligned with the
OpenClaw `dialpad-draft-callback` plugin `callbackUrl` config. The default
`host.docker.internal` URL is for gateway-container to host-webhook delivery.

