Low-level Graph API client for the WhatsApp Business Cloud API: authenticated JSON, multipart, and binary requests, response unwrapping, and URL building.
Per-call credentials. Every API-calling function in this package
takes an optional trailing opts Map, resolved by resolve: opts
first, then context variables, then defaults. Nothing from opts is
written to ctx, so one run can act for many businesses and parallel
flows don't share credentials. An access-token from opts is masked
in call records. An access-token present but blank or null is an
error, never a fallback to whatsapp.access.token.
access-token: the access token for this callphone-number-id: the business number to use when a request has nophone_number_idapi-version: the Graph API version for this call
Context Vars
whatsapp.access.tokenwhatsapp.api.version
Values
USER_AGENT
USER_AGENT: Str "hot-whatsapp/1.1.0"
Functions
auth-headers
fn (access-token: Str): Map
download
fn (url: Str): Bytes
fn (url: Str, access-token: Str): Bytes
fn (url: Str, opts: Map): Bytes
Authenticated GET returning the raw response bytes (Bytes) on 2xx, or an Err carrying HttpError. Used for media downloads.
whatsapp.access.token
error-message
fn (error: Any): Str?
The human-readable message from a failed call's error value: Meta's
body.error.message when the body is a Graph error object, otherwise
the stringified body. Returns null for a non-error value.
Example
sent ::whatsapp::messages/send-text(::whatsapp::messages/SendTextRequest({
phone_number_id: "109876543210", to: "15551234567", text: "Hi"
}))
if-err(sent, (e) { println(::whatsapp::api/error-message(e)) })
handle-response
fn (response: Result): Any
Unwrap an HTTP response: the decoded body on 2xx, an Err carrying HttpError otherwise. Callers return transport Errs before calling it. Exposed for testing.
non-blank
fn (value: Any): Str?
request
fn (method: Str, url: Str): Any
fn (method: Str, url: Str, opts: Map): Any
fn (method: Str, url: Str, additional-headers: Map, body: Map): Any
fn (method: Str, url: Str, additional-headers: Map, body: Map, access-token: Str): Any
fn (method: Str, url: Str, additional-headers: Map, body: Map, opts: Map): Any
Call the Graph API with a JSON body. The access token comes from per-call opts (see resolve), the whatsapp.access.token context variable, or the explicit 5-arity form, and is sent as a Bearer token. Null fields in body are not sent. Returns the decoded body on 2xx, or an Err carrying HttpError.
Example
::api ::whatsapp::api
// GET request
profile ::api/request("GET", ::api/url("109876543210/whatsapp_business_profile"))
// POST request with JSON body
sent ::api/request("POST", ::api/url("109876543210/messages"), {}, {messaging_product: "whatsapp", to: "15551234567", type: "text", text: {body: "Hello"}})
// With per-call credentials
tenant {access-token: "tenant-b-token"}
other ::api/request("GET", ::api/url("109876543210/whatsapp_business_profile", tenant), tenant)
whatsapp.access.token
request-bytes
fn (method: Str, url: Str, content-type: Str, body: Bytes): Any
fn (method: Str, url: Str, content-type: Str, body: Bytes, access-token: Str): Any
fn (method: Str, url: Str, content-type: Str, body: Bytes, opts: Map): Any
Send a raw body (e.g. a multipart upload built as Bytes) with an explicit Content-Type. Returns the decoded JSON body on 2xx, or an Err carrying HttpError.
whatsapp.access.token
resolve
fn (opts: Map): Map
Credentials for one call (see resolve-from). An access-token from
opts is masked in call records with ::hot::ctx/mark-secret, and
opts values are never written to ctx. The API version comes from
::whatsapp::core/api-version(opts).
Example
::whatsapp::api/resolve({access-token: "tenant-b-token", phone-number-id: "109876543210"}).phone-number-id
whatsapp.access.token
resolve-from
fn (opts: Map, ctx-values: Map): Map
The one resolver, pure: {access-token} for a call from opts first, then ctx-values (a Map of context variables), plus phone-number-id when opts has one. An Err when no token is set, or when access-token is in opts but blank or null (that never falls back to context). Exposed for testing; resolve feeds it the context.
segment
fn (value: Any): Str
Percent-encode one path segment (an id or name). Exposed for testing.
send-json
fn (method: Str, url: Str, additional-headers: Map, body: Map, access-token: Str): Any
url
fn (path: Str): Str
fn (path: Str, opts: Map): Str
Build a Graph API URL from a path relative to the versioned base, e.g. url("123/messages") gives https://graph.facebook.com/v26.0/123/messages. The version follows ::whatsapp::core/api-version, with opts when given.
with-phone-number
fn (phone-number-id: Str?, opts: Map, action: Fn): Any
Run action with the URL-encoded business number for a call: phone-number-id when set, else phone-number-id in opts. An Err when neither is set.
Types
HttpError
HttpError type {
status: Int,
headers: Map,
body: Any
}
A non-2xx Graph API response. Meta's error object is in body.error (message, type, code, error_subcode, fbtrace_id); error-message extracts the message safely.