DialerBee APIv1 · 1.0.0-rc6

JavaScript and TypeScript SDK

Official client for the DialerBee public API, version 1 (https://app.dialerbee.com/api/v1). Typed models, RFC 9457 errors with the request id, safe retries, an idempotency helper, cursor auto-pagination, streamed recordings and webhook signature verification.

Status: pre-release. This package is not yet published to npm. It is generated from the API contract version named in contract-lock.json (1.0.0-rc6); until version 1.0.0 is final, shapes may still change. Answers may gain fields within v1 (ignore fields you do not know); requests with a field the API does not know are refused.

Server-side only. An API key is a secret. The client refuses to start in a browser.

Requires Node.js 18.17 or later (uses the built-in fetch and node:crypto). No runtime dependencies.

Quick start

import { DialerBee } from 'dialerbee';

const db = new DialerBee({ apiKey: process.env.DIALERBEE_API_KEY });

const me = await db.me.get();            // works with any active key; shows your scopes
console.log(me.client.name, me.scopes);

const result = await db.leads.import(
  { leads: [{ phone: '+12125550101', external_id: 'CRM-1', full_name: 'Ann Lee' }] },
  { campaign_id: 'cmp_0f8fad5b-d9cb-469f-a165-70867728950e' },
);
for (const row of result.data) console.log(row.index, row.status, row.reason);

If apiKey is left out, the client reads DIALERBEE_API_KEY.

Calling convention

Every operation of the contract is a method named after its route id: leads.import, campaigns.pause, webhooks.rotateSecret, webhookEvents.replay, sms.deliveryReports ... (the full map is exported as RESOURCE_METHODS, operationId -> resource.method).

method(...pathIds, body?, query?, options?)

Each method returns the data. <method>WithResponse(...) returns { data, status, requestId, etag, idempotencyKey, idempotencyReplayed, headers }. db.request(operationId, { path, query, body }, options) is the low-level call behind all of them.

Resources: me, leads, campaigns, calls, webhooks, webhookEvents, dnc, callbacks, appointments, sms, whatsapp, consents, reports.

Idempotency

Every POST and DELETE that changes data needs an Idempotency-Key. When you do not pass idempotencyKey, the SDK generates a UUID, reuses it on every retry, and returns it (idempotencyKey on the response and on every error). Pass your own key when a logical operation may be retried across process restarts. PATCH accepts a key; without one a PATCH is never retried.

const key = newIdempotencyKey();                 // store it with your job
await db.campaigns.pause(campaignId, { idempotencyKey: key });

Same key, same request: the stored answer (idempotencyReplayed: true). Same key, different request: IdempotencyConflictError.

Retries

Automatic, bounded and only when safe:

dnc.check is a POST that only reads: it changes nothing, so it is retried like a GET, without an Idempotency-Key. Any other POST, PATCH or DELETE sent without a key is never retried. maxRetries: 0 (client or call option) turns retries off.

Errors

Branch on status and code, never on message or detail. Every API error carries requestId (quote it to support), type, title, detail, errors (field problems of a 400/422), retryAfter, operationId, routeId and the idempotencyKey that was sent.

Class When
AuthenticationError 401 invalid_api_key (missing, unknown, expired or revoked key: one answer for all)
PermissionDeniedError 403 (scope missing, Tier-2 switch off, IP not allowed, access disabled)
NotFoundError 404 (includes another account's objects)
ConflictError 409 state conflicts
IdempotencyConflictError 409 idempotency_conflict
OutcomeUnknownError 409 outcome_unknown: see below
PreconditionFailedError 412 / 428 (If-Match)
ValidationError 400, 413, 415, 422
RateLimitError 429
ServerError 5xx
ApiError any other API answer (base class of all the above)
ConnectionError, TimeoutError no HTTP answer
InvalidArgumentError refused locally; nothing was sent
OperationBlockedError the operation is not available yet for any account; nothing was sent (today: whatsapp.send)

outcome_unknown: DialerBee could not confirm whether the write happened. Do not repeat the operation with a new key. Check the resource, or contact support with the requestId; the same key keeps answering this until DialerBee has reconciled it.

try {
  await db.leads.import(body, { campaign_id }, { idempotencyKey: key });
} catch (e) {
  if (e instanceof OutcomeUnknownError) { /* keep `key`; check later; never re-send with a new key */ }
  else if (e instanceof ApiError) console.error(e.status, e.code, e.requestId);
  else throw e;
}

Lists

const page = await db.leads.list({ campaign_id, limit: 100 });   // one page: { data, page }
for await (const lead of db.leads.iterate({ campaign_id })) {    // every item, page after page
  console.log(lead.id, lead.status);
}

iterate follows page.next_cursor with the same filters and stops when has_more is false. Cursors are opaque and bound to your key, client and filters.

Updating with If-Match

const r = await db.leads.getWithResponse(leadId);
await db.leads.update(leadId, { email: '[email protected]' }, { ifMatch: r.etag ?? undefined });

Recordings

const rec = await db.calls.recording(callId);                    // streamed, never a storage URL
await pipeline(Readable.fromWeb(rec.body), createWriteStream('call.mp3'));
const part = await db.calls.recording(callId, { range: 'bytes=0-65535' });   // 206, part.contentRange

Call summaries

Every call of your account answers a summary with a status (needs the summaries:read scope):

const s = await db.calls.summary(callId);
if (s.status === 'ready') console.log(s.summary, s.next_action, s.language);
else if (s.status === 'not_generated') console.log('no summary:', s.not_generated_reason);
// 'processing': ask again later. Treat a not_generated_reason you do not know as "not generated".

Webhooks

Verify the raw body (the exact bytes received), not a re-parsed JSON. During the 24 h after a secret rotation DialerBee sends two signatures; either one is accepted. Pass both secrets while you roll over.

import express from 'express';
import { verifyWebhook, WebhookVerificationError } from 'dialerbee';

app.post('/dialerbee/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
  try {
    const event = verifyWebhook({ rawBody: req.body, headers: req.headers, secrets: [process.env.DIALERBEE_WEBHOOK_SECRET] });
    if (event.type === 'call.completed') { /* event.data.duration_seconds ... */ }
    res.sendStatus(204);                        // de-duplicate on event.id (retries and replays reuse it)
  } catch (e) {
    if (e instanceof WebhookVerificationError) return res.sendStatus(400);
    throw e;
  }
});

The signing secret is returned once: by webhooks.create and by webhooks.rotateSecret. Store it then. DialerBee does not keep it, so a replay of the same request (same Idempotency-Key, idempotencyReplayed: true) answers secret: null; the type is string | null. If you lost the secret, rotate it again with a new key.

isValidWebhookSignature(...) returns a boolean instead; buildSignatureHeader / computeWebhookSignature reproduce DialerBee's signature (for your own tests). Timestamps more than 300 s from your clock are refused.

Tier 2 and blocked operations

Tier-2 operations (campaign resume, callback and appointment writes, consent, SMS send) can reach or unlock real people. They need their own scope and an account switch that DialerBee turns on only on request; otherwise they answer 403 tier_not_enabled (never retried).

The Do-Not-Call list can be checked (dnc.check) and added to (dnc.add) through the API. Removing a number from it is not offered in API v1: it is done in the DialerBee console, by an account administrator.

sms.send is Tier 2: it needs the scope messages:send (a sending key from the account's guided sending setup) and the account's switch. Every message needs your own client_reference; a second message with the same reference answers 409 duplicate_client_reference and nothing is sent again.

Click-to-call, speed-to-lead and WhatsApp

Logging

new DialerBee({ logger: { debug: console.debug, warn: console.warn } });

The logger gets the operation, route id, attempt, status, code, request id and retry delay. It never gets the key, headers or bodies (bodies hold personal data). util.inspect, JSON.stringify and String of the client show [REDACTED] for the key.

Options

Option Default
apiKey DIALERBEE_API_KEY dbk_live_... / dbk_test_...
baseUrl https://app.dialerbee.com/api https only (plain http for localhost)
timeoutMs 30000 per attempt
maxRetries 2 3 attempts in total
fetch global fetch
logger none
appInfo none appended to User-Agent
dangerouslyAllowBrowser false never ship a key to a browser