butt-dial-sdk
v0.5.0
Published
Client-side tunnel SDK for the Butt-Dial communication service — outbound, inbound callbacks, retry orchestration, and end-to-end diagnostic. Node/TypeScript port of the Python butt-dial-sdk.
Downloads
530
Maintainers
Readme
butt-dial-sdk (Node)
A stateless client-side tunnel to the Butt-Dial communication service for Node.js / TypeScript hosts. Ships outbound messaging, inbound callbacks, retry orchestration, and an end-to-end diagnostic CLI.
Status: 0.1.0 — first Node release. Wire-protocol parity with the Python SDK (0.4.4); same audit (rounds 1–7, F01–F92) governs both.
Install
npm install butt-dial-sdkNode 18.17+ (for native fetch and crypto.timingSafeEqual).
Building a host app? Run
npx buttdial guidefor the agent-readable contract — patterns to use, anti-patterns to avoid. Runnpx buttdial example listto see the canonical patterns;npx buttdial errorsfor the typed-exception reference.
Identity model — host owns the agent UUID
The host application generates the UUID for each agent and tells BD at registration. BD echoes it back along with a minted per-agent bearer token. Three tokens, three jobs:
| Token | Held by | Used for |
|---|---|---|
| teamToken | host backend | AccountsClient — provisioning, registration, billing, lifecycle |
| agentToken | per-agent runtime | Agent — sending and receiving on this agent's number |
| agentId | host-supplied UUID | identifies the agent across both surfaces |
Quickstart — register an agent and send a message
import { randomUUID } from "node:crypto";
import { AccountsClient, Agent } from "butt-dial-sdk";
// 1. Workspace admin: register an agent (host owns the UUID).
const accounts = new AccountsClient({
baseUrl: "https://call.95percent.ai",
teamToken: TEAM_TOKEN,
});
const agentId = randomUUID();
const out = await accounts.registerAgent(agentId, "Maya");
const agentToken = out.token; // BD minted; persist to your vault
// 2. Per-agent runtime: send a message.
const maya = new Agent({
baseUrl: "https://call.95percent.ai",
agentId,
agentToken,
});
const result = await maya.sendMessage({
to: "+14155550123",
body: "Hello from Maya",
idempotencyKey: `msg-${randomUUID()}`,
});
console.log(result.messageSid);Or, for single-agent setups, pull from the environment:
export BUTT_DIAL_BASE_URL=https://call.95percent.ai
export BUTT_DIAL_AGENT_ID=<the UUID your host generated>
export BUTT_DIAL_AGENT_TOKEN=<token BD minted at registration>import { Agent } from "butt-dial-sdk";
const maya = Agent.fromEnv();See examples/ for full working code.
What's included
| Component | What it does |
|---|---|
| Agent | Per-agent runtime. sendMessage() plus 13 typed WhatsApp action helpers (react, edit, forward, deleteMessage, typing, markRead, sendPoll, sendButtons, sendLocation, sendContact, postStatus, setChatTtl, sendViewOnce). MCP/SSE transport via @modelcontextprotocol/sdk with 3-attempt exponential backoff. |
| InboundHandler | Decorator-style callbacks (onMessage, onDeliveryReceipt, onStatusUpdate). Ships WhatsApp payload parser + signature verification + subscription-challenge handler. Express middleware adapter included. |
| RetryWorker | Background retry loop against a host-provided FailedMessageRepository interface. Pluggable 2^n-minute backoff. Dead-letter transition with optional onDeadLetter hook. |
| AccountsClient | Workspace admin REST wrapper. Onboarding + token lifecycle (register → verify-email → setPhone → OTP → confirmPhone → rotateToken) plus per-agent provisioning (registerAgent accepts a host-supplied agentId, BD echoes it back with the minted agentToken). Typed exceptions per server error code. |
| verifyWebhookSignature | Stripe-style HMAC-SHA256 verifier for the account.verified webhook BD posts after a user clicks their verification link. 5-minute replay protection, constant-time compare via crypto.timingSafeEqual. |
| buttdial CLI | guide, example list/show/extract, errors, doctor subcommands. Bundled docs and examples ship inside the npm package. |
Onboard a tenant — the canonical pattern
For host apps that auto-register a Butt-Dial workspace when a customer signs up: pass branding / returnUrl / webhookUrl to register(). The verification email is branded as your product, the verify page redirects users back to you, and BD posts back to your webhook the moment the user clicks. No polling, no "did it work?" dead-end.
See examples/with-onboarding-handshake/ for a complete Express implementation.
import { AccountsClient } from "butt-dial-sdk";
import { verifyWebhookSignature } from "butt-dial-sdk/webhooks";
const accounts = new AccountsClient({ baseUrl: "https://call.95percent.ai" });
// 1. Register the workspace (auto-fired from your /signup handler).
const resp = await accounts.register({
orgName: "Acme Inc",
ownerEmail: "[email protected]",
branding: {
logoUrl: "https://acme.com/logo-mark.svg",
primaryColor: "#00C9A7",
productName: "Acme",
supportEmail: "[email protected]",
},
returnUrl: "https://acme.com/welcome",
webhookUrl: "https://acme.com/api/webhooks/butt-dial/account-verified",
});
// Capture once — webhookSecret is NOT retrievable later.
await vault.put(workspaceId, "bdWebhookSecret", resp.webhookSecret);
await vault.put(workspaceId, "bdPollToken", resp.pollToken);
// 2. Receive the webhook when the user clicks (Express).
app.post(
"/api/webhooks/butt-dial/account-verified",
express.raw({ type: "application/json" }),
async (req, res) => {
const ok = verifyWebhookSignature({
rawBody: req.body,
timestampHeader: String(req.headers["x-bd-timestamp"] ?? ""),
signatureHeader: String(req.headers["x-bd-signature"] ?? ""),
secret: await vault.get(workspaceId, "bdWebhookSecret"),
});
if (!ok) return res.status(401).json({ error: "invalid signature" });
const body = JSON.parse(req.body.toString("utf8"));
workspace.bdTeamToken = body.teamToken;
workspace.provisioned = true;
await workspace.save();
res.json({ ok: true });
},
);Idempotency: dedupe webhook deliveries by the X-BD-Delivery-Id header. BD retries (1s / 5s / 25s / 2m / 10m) until you 2xx, then dead-letters.
Smoke-test only / dev? The bare register({orgName, ownerEmail}) form still works but emits process.emitWarning(..., 'DeprecationWarning') on every call: it produces a BD-branded email and gives no completion signal. Don't ship a host app with the bare form.
CLI
npx buttdial guide # full integration manual (AGENTS.md)
npx buttdial example list # canonical patterns
npx buttdial example show with-onboarding-handshake
npx buttdial example extract with-onboarding-handshake ./my-app/
npx buttdial errors # typed-exception reference
npx buttdial doctor --to +1... # 5-stage live diagnosticThe doctor exits non-zero on any failed stage — wire it into CI as a live integration canary.
Design principles
- Stateless — SDK owns no DB, no files, no global state beyond a configured
Agent/AccountsClient. - Interfaces, not classes — host implements
FailedMessageRepositoryas a plain TS interface. SDK never queries the DB. - Errors are reported, not thrown —
SendResult.failed/error/errorCode/stageFailed/remediationis the primary surface. The few places the SDK throws inherit fromButtDialError. - Async-first — built on native
fetch,@modelcontextprotocol/sdk, nativecrypto.
License
MIT — free for commercial use, no obligations.
