import { type MetaAccountParams } from './accounts.ts'
import { graphItems, graphRequest, requireConfirmOrDryRun, type JsonObject } from './core.ts'
import { boundedLimit } from './validation.ts'
export type WhatsAppSendParams = MetaAccountParams & {
phoneNumberId: string
to: string
body?: string
previewUrl?: boolean
templateName?: string
templateLanguage?: string
templateComponents?: unknown[]
dryRun?: boolean
confirm?: boolean
}
/** List Businesses the token can access. Use these ids to find WhatsApp accounts — never hard-code one. */
export async function listBusinesses(params: MetaAccountParams & { limit?: number; after?: string } = {}) {
const data = await graphRequest<JsonObject>({
...params,
path: '/me/businesses',
query: {
fields: 'id,name,verification_status',
limit: boundedLimit(params.limit, 25, 100),
after: params.after,
},
})
return graphItems<JsonObject>(data)
}
/** List WhatsApp Business accounts owned by a Business. */
export async function listWhatsAppAccounts(
params: MetaAccountParams & { businessId: string; limit?: number; after?: string },
) {
const businessId = params.businessId?.trim()
if (!businessId) throw new Error('listWhatsAppAccounts requires businessId from listBusinesses.')
const data = await graphRequest<JsonObject>({
...params,
path: '/' + encodeURIComponent(businessId) + '/owned_whatsapp_business_accounts',
query: {
fields: 'id,name,currency,timezone_id',
limit: boundedLimit(params.limit, 25, 100),
after: params.after,
},
})
return graphItems<JsonObject>(data)
}
/** List Cloud API phone numbers on a WhatsApp Business account. */
export async function listPhoneNumbers(
params: MetaAccountParams & { wabaId: string; limit?: number; after?: string },
) {
const wabaId = params.wabaId?.trim()
if (!wabaId) throw new Error('listPhoneNumbers requires wabaId from listWhatsAppAccounts.')
const data = await graphRequest<JsonObject>({
...params,
path: '/' + encodeURIComponent(wabaId) + '/phone_numbers',
query: {
fields: 'id,display_phone_number,verified_name,quality_rating,code_verification_status',
limit: boundedLimit(params.limit, 25, 100),
after: params.after,
},
})
return graphItems<JsonObject>(data)
}
/** Read one WhatsApp Cloud phone number. */
export async function getPhoneNumber(params: MetaAccountParams & { phoneNumberId: string }) {
const phoneNumberId = params.phoneNumberId?.trim()
if (!phoneNumberId) throw new Error('getPhoneNumber requires phoneNumberId from listPhoneNumbers.')
return graphRequest<JsonObject>({
...params,
path: '/' + encodeURIComponent(phoneNumberId),
query: {
fields: 'id,display_phone_number,verified_name,quality_rating,code_verification_status',
},
})
}
/** List message templates on a WhatsApp Business account. */
export async function listTemplates(
params: MetaAccountParams & { wabaId: string; limit?: number; after?: string },
) {
const wabaId = params.wabaId?.trim()
if (!wabaId) throw new Error('listTemplates requires wabaId.')
const data = await graphRequest<JsonObject>({
...params,
path: '/' + encodeURIComponent(wabaId) + '/message_templates',
query: {
fields: 'id,name,language,status,category',
limit: boundedLimit(params.limit, 25, 100),
after: params.after,
},
})
return graphItems<JsonObject>(data)
}
function sendPayload(params: WhatsAppSendParams): JsonObject {
const to = params.to.replace(/\s+/g, '')
if (!to) throw new Error('sendMessage requires to (E.164 digits, no spaces).')
if (params.templateName?.trim()) {
return {
messaging_product: 'whatsapp',
to,
type: 'template',
template: {
name: params.templateName.trim(),
language: { code: params.templateLanguage?.trim() || 'en_US' },
components: params.templateComponents ?? [],
},
}
}
const body = params.body?.trim()
if (!body) throw new Error('sendMessage requires body for a text message, or templateName for a template.')
return {
messaging_product: 'whatsapp',
to,
type: 'text',
text: { body, preview_url: params.previewUrl === true },
}
}
/**
* Preview or send a WhatsApp Cloud message.
* Live send requires confirm: true. This helper never sends without that confirmation.
*/
export async function sendMessage(params: WhatsAppSendParams) {
const phoneNumberId = params.phoneNumberId?.trim()
if (!phoneNumberId) {
throw new Error('sendMessage requires phoneNumberId from listPhoneNumbers. Do not invent a phone number id.')
}
const body = sendPayload(params)
const path = '/' + encodeURIComponent(phoneNumberId) + '/messages'
const mode = requireConfirmOrDryRun({
dryRun: params.dryRun,
confirm: params.confirm,
action: 'sendMessage',
})
if (mode.dryRun) {
return { dryRun: true as const, method: 'POST', path, body }
}
const result = await graphRequest<JsonObject>({
...params,
method: 'POST',
path,
body,
})
return { sent: true as const, to: body.to, result }
}
/**
* WhatsApp Cloud API helpers.
* @example
* import { listBusinesses, sendMessage } from 'kody:@kody/meta/whatsapp'
* const businesses = await listBusinesses()
* const preview = await sendMessage({ phoneNumberId, to, body: 'Hi', dryRun: true })
*/
export default function whatsapp() {
return {
listBusinesses,
listWhatsAppAccounts,
listPhoneNumbers,
getPhoneNumber,
listTemplates,
sendMessage,
}
}