packages/smartsend is a typed fetch client for SmartSend, the WhatsApp Business provider. It is one file, src/index.ts, with no dependencies beyond Node’s crypto.
This page documents the package’s exports. How a studio connects WhatsApp and how messages flow through the product belongs to the integration guides.
Construction
server.ts and exposes it as ctx.smartsend. @repo/auth builds its own short lived client inside sendLoginOtpOverWhatsapp from process.env, without a webhook secret.
The client is stateless. config.apiKey is only a default for the legacy methods. Most calls name the organization they act for explicitly.
Two credential styles
SmartSend has an older lane and a newer one, and the client uses both.
The header style keeps the credential out of URLs and access logs. In this codebase the value passed as
organizationId is the org’s SmartSend API key. For OTP sends that is SMARTSEND_OTP_API_KEY or SMARTSEND_API_KEY. For studio traffic it is the key the studio stored when it connected WhatsApp.
Methods
sendMessage(input)
POST /whatsapp/api/OpenAction/send-message. Free form text to a phone number or a chat id, with an optional media URL. Throws smartsend send failed: <status> <body> on a non 2xx response.
delivered: true means SmartSend accepted the request. It is not a delivery receipt from WhatsApp.
sendTemplate(input)
POST /integrations/make/messages/send-template. Sends an approved WhatsApp template. This is how OTP codes go out: template registercode with the code as the body parameter and the URL button parameter. Throws on a non 2xx response.
sendConversationMessage(input)
POST /organizations/agent/message/send. Replies inside an existing SmartSend conversation. Used by the WhatsApp agent to answer in the thread the coach wrote in. Response errors are raised by readData.
listUsers({ organizationId })
GET /integrations/make/rpc/users. Returns { id, value } rows, the SmartSend users of the organization. Used to map coaches to SmartSend users.
listTemplates({ organizationId })
GET /integrations/make/rpc/templates. Returns { id, value } rows where id is the template name, the value sendTemplate expects, and value is a label that includes the language.
templateParams({ organizationId, templateName, languageCode? })
GET /integrations/make/rpc/template-params. Returns the fillable parameters of one template in send order, each { name, label, type? }. Rows without a name are dropped. A response without a data array returns an empty list.
listMailingLists({ organizationId })
GET /lists. Returns { id, value } rows with the list name as value.
createMailingList(input)
POST /listswith the name. A list is created empty.POST /lists/:id/recipientsonce per contact, with a concurrency of 5. SmartSend has no bulk variant.
rejected. If every contact fails, the list is deleted again and the method throws smartsend add-recipient failed for every contact, because an empty list is worse than none.
The method uses the root /lists API on purpose. Lists written through the /integrations/make/lists/create endpoint land in a store the SmartSend web app cannot read, so a coach following a link to one is told it does not exist.
registerInboundWebhook(input) and unregisterInboundWebhook(input)
POST /whatsapp/api/Trigger/create-receive-message-trigger and DELETE /whatsapp/api/Trigger/unsubscribe. Tell SmartSend to post inbound messages for an org to hookUrl. The WhatsApp module builds that URL from PUBLIC_API_URL, the studio id and the webhook token.
parseInbound(payload)
Normalizes what SmartSend posts to the webhook. Accepts one object or an array. Entries without a string senderId and a string text are skipped.
isGroup. The caller decides whether to drop them.
verifyWebhookToken(token)
Returns true only when a webhook secret is configured, a token was supplied, both have the same length, and timingSafeEqual matches. With SMARTSEND_WEBHOOK_SECRET empty every webhook is rejected.
Both webhook routes in core-api call it on the token query parameter. See Route lanes.
Failure behaviour
The client throws plain
Error, not AppError. A service that calls it decides what the user sees. The OTP flows catch the error, log it and answer SERVICE_UNAVAILABLE.
There are no retries and no timeouts in the client. A hung SmartSend request holds the calling request until the socket gives up. The methods take no abort signal. If you call one from a request path where that matters, race it against a timer at the call site or move the send off the request.
Testing with it
SmartSendClient is a plain object type. Tests pass an object with only the methods the code under test calls:
Rebuilding
pnpm --filter @perform/smartsend build. A missing method on SmartSendClient in a core-api type check after a pull means the package’s dist is stale.