Skip to content

Kody is live

Watch the launch video — what Kody is, and why it exists.

← Public packages

@kody/blandPublic

src/send-call.ts

99 lines · 2.5 KB · TypeScript
/**
 * Place an outbound Bland AI phone call (`POST /v1/calls`).
 * Dry-run by default — pass `confirm: true` to dial live.
 * Provide `task` or `pathway_id` (not both required together; Bland needs one).
 *
 * @param input - phone_number plus task or pathway_id, optional voice/webhook
 * @returns Call create response (`call_id`) or dry-run preview
 *
 * @example
 * import { sendCall } from 'kody:@kody/bland/send-call'
 * await sendCall({
 *   phone_number: '+15551234567',
 *   task: 'Ask whether they carry 10mm hex keys. Thank them and hang up.',
 *   voice: 'maya',
 *   max_duration: 5,
 *   confirm: true,
 * })
 */
import { blandRequest, type BlandAuthInput, type BlandObject } from './core.ts'
import { packageStorage } from 'kody:runtime'
import { SETTINGS_DEFAULT_VOICE } from './core.ts'

export type SendCallInput = BlandAuthInput & {
	body?: BlandObject
	phone_number?: string
	task?: string
	pathway_id?: string
	pathway_version?: number
	voice?: string
	max_duration?: number
	webhook?: string
	webhook_events?: string[]
	keywords?: string[]
	from?: string
	voicemail?: BlandObject
	wait_for_greeting?: boolean
	record?: boolean
	dryRun?: boolean
	confirm?: boolean
}

async function defaultVoice(): Promise<string | null> {
	try {
		const v = String((await packageStorage().get(SETTINGS_DEFAULT_VOICE)) ?? '').trim()
		return v || null
	} catch {
		return null
	}
}

function buildBody(input: SendCallInput, voiceFallback: string | null): BlandObject {
	if (input.body && typeof input.body === 'object') {
		return { ...input.body }
	}
	const body: BlandObject = {}
	const keys = [
		'phone_number',
		'task',
		'pathway_id',
		'pathway_version',
		'voice',
		'max_duration',
		'webhook',
		'webhook_events',
		'keywords',
		'from',
		'voicemail',
		'wait_for_greeting',
		'record',
	] as const
	for (const key of keys) {
		const value = input[key]
		if (value !== undefined) body[key] = value
	}
	if (body.voice == null && voiceFallback) body.voice = voiceFallback
	return body
}

export async function sendCall(input: SendCallInput = {}) {
	const voiceFallback = await defaultVoice()
	const body = buildBody(input, voiceFallback)
	const live = input.confirm === true && input.dryRun !== true
	if (live) {
		if (!body.phone_number) {
			throw new Error('phone_number is required (E.164, e.g. +15551234567).')
		}
		if (!body.task && !body.pathway_id) {
			throw new Error('Provide task (prompt) or pathway_id before placing a live call.')
		}
	}
	return blandRequest({
		...input,
		method: 'POST',
		path: '/v1/calls',
		body,
	})
}

export default sendCall