Skip to main content
Version: v0.23.1

Plugin Catalog

The ten official plugins maintained in the openwa-plugins marketplace repository. Each entry lists what the plugin does, the permissions it declares, its configuration keys, and the minimum OpenWA version it requires.

Every plugin here is an extension that runs sandboxed in a worker thread and hooks message:received (some hook more) — except supabase-otp-hook, which declares no hooks at all and is driven entirely by its ingress route. For the runtime model and the permission system, see Plugin Architecture. To install and configure a plugin, see Plugins Overview.

Summary

PluginDoesPermissionsStatusRequires
after-hoursAway reply outside business hoursmessages:sendstable≥ 0.7.0
chat-flowStateful numbered-menu botmessages:send, storage:usestable≥ 0.7.0
chatwoot-adapterTwo-way Chatwoot inbox sync + handovernet:fetch, conversation:send, webhook:ingress, engine:read, storage:usestable≥ 0.8.7
faq-botKeyword/regex auto-replymessages:sendstable≥ 0.6.1
group-translateIn-group auto-translationmessages:send, engine:read, net:fetch, storage:usestable≥ 0.7.0
gsheets-loggerLogs message events to a Google Sheetnet:fetch, storage:usestable≥ 0.7.0
http-actionRun REST API calls from chat commandsnet:fetch, conversation:send, storage:usestable≥ 0.8.0
supabase-otp-hookDeliver Supabase phone OTPs over WhatsAppwebhook:ingress, messages:sendbeta≥ 0.8.16
typebot-connectorRun a Typebot flow as the bot brainnet:fetch, conversation:send, storage:usestable≥ 0.8.2
voice-transcriptionVoice notes → text webhook eventnet:fetch, messages:send, storage:usebeta≥ 0.7.0

Requires is the minOpenWAVersion. All ten are documented at the versions published in the marketplace catalog; column values come from each plugin's manifest.json.

Permissions glossary

messages:send lets the plugin send and reply to messages. conversation:send is the integration-SDK capability that relays messages back into a chat (used by the adapters/connectors that own their own send path). engine:read lets a plugin read engine state such as group info. net:fetch lets it make outbound HTTP calls through the host's SSRF-guarded fetch, restricted to hosts in the manifest's net.allow list. webhook:ingress lets a plugin expose a verified inbound URL (signed by the caller — for example Chatwoot HMAC or Standard Webhooks) that the host authenticates before the plugin runs. storage:use lets a plugin read and write its own key-value store through ctx.storage; it is required from OpenWA v0.17.0, and before that release the store was reachable with no permission declared at all. A plugin that declares no permissions can only read hook events.

Upgrade these plugins before upgrading the gateway to v0.17.0

The seven plugins that declare storage:use above need at least the versions listed in their tables below — chatwoot-adapter 0.9.1, chat-flow 1.1.2, group-translate 1.3.1, gsheets-logger 0.3.3, http-action 0.2.2, typebot-connector 0.2.2, voice-transcription 1.2.3 — installed before the gateway moves to v0.17.0. The Requires column does not encode this: none of the seven raised its minOpenWAVersion, because the new versions run unchanged on an older gateway, which ignores a permission string it does not recognise. It is the older plugin versions that break on v0.17.0, and they break quietly: the denial lands at the plugin's next storage call rather than at load, and a plugin that stores during onEnable ends up in ERROR instead of merely losing the write. after-hours, faq-bot, and supabase-otp-hook never touch ctx.storage and need no upgrade.

after-hours

Replies with a configurable away message to messages received outside business hours.

FieldValue
Identifierafter-hours
Version0.2.1
Statusstable
Requires OpenWA≥ 0.7.0 (tested 0.14.0)
Typeextension
Permissionsmessages:send
Hooksmessage:received
RepositoryOpenWA-plugins/after-hours

What it does. Holds a per-weekday business-hours schedule interpreted in a configurable IANA timezone. When a message arrives outside the open window for that day, it sends the configured away message as a quoted reply, throttled to at most once per chat per cooldownSec. Group chats are ignored unless respondInGroups is set. A malformed schedule or unknown timezone fails fast and shows as ERROR in the dashboard rather than misbehaving silently.

Configuration.

KeyRequiredDefaultDescription
scheduleyesJSON object mapping mon..sun to "HH:MM-HH:MM" (24-hour, open < close) or null for closed. An absent day is closed.
timezonenoUTCIANA timezone the schedule is interpreted in, e.g. Asia/Jakarta.
awayMessageyesReply sent outside business hours.
cooldownSecno3600Minimum seconds between after-hours replies to the same chat. 0 replies every time.
respondInGroupsnofalseWhether to reply in group chats.

Example schedule:

{ "mon": "09:00-17:00", "tue": "09:00-17:00", "wed": "09:00-17:00",
"thu": "09:00-17:00", "fri": "09:00-17:00", "sat": "09:00-13:00", "sun": null }

chat-flow

An interactive, stateful auto-reply: a trigger word opens a greeting plus a numbered menu, and replies walk a configurable menu tree.

FieldValue
Identifierchat-flow
Version1.1.2
Statusstable
Requires OpenWA≥ 0.7.0 (tested 0.14.0)
Typeextension
Permissionsmessages:send, storage:use
Hooksmessage:received
RepositoryOpenWA-plugins/chat-flow

What it does. A trigger word (or any message, if trigger is empty) sends a greeting and a numbered menu. The user's reply selects an option; selections traverse a menu tree of arbitrary depth, where leaf nodes end the flow. State is kept per (session, chat) and expires after 15 minutes of inactivity; re-sending the trigger restarts an active flow. A reply that matches no option re-sends the current menu. The plugin is session-scoped (sessionScoped) and ships a visual flow editor (configUi) the dashboard opens in a sandboxed frame, so the tree can be designed without hand-editing JSON.

Configuration.

KeyRequiredDefaultDescription
greetingyesThe greeting plus menu sent when the flow starts.
optionsnoThe menu tree: an array of { key, text, options? } nodes that nest arbitrarily.
triggernoWord that starts the flow (case-insensitive). Empty means any message starts it.
respondInGroupsnofalseWhether to run in group chats.

Example options tree:

{
"trigger": "menu",
"greeting": "Hi! Reply with a number:\n1. Pricing\n2. Support",
"options": [
{ "key": "1", "text": "Plans start at Rp100.000/mo." },
{ "key": "2", "text": "Support — reply with a number:\n1. Billing\n2. Technical",
"options": [
{ "key": "1", "text": "Billing: billing@example.com" },
{ "key": "2", "text": "A ticket has been created — we'll reply shortly." }
] }
]
}

chatwoot-adapter

Two-way sync between a WhatsApp session and a Chatwoot inbox, with human handover — relay WhatsApp into Chatwoot, send agent replies back, and silence other OpenWA bots on that chat while a human agent is in control.

FieldValue
Identifierchatwoot-adapter
Version0.9.1
Statusstable
Requires OpenWA≥ 0.8.7 (tested 0.14.0)
Typeextension
Permissionsnet:fetch, conversation:send, webhook:ingress, engine:read, storage:use
Hooksmessage:received, message:sent
RepositoryOpenWA-plugins/chatwoot-adapter

What it does. Binds one Chatwoot account (apiToken + accountId + inboxId) to a WhatsApp session. Inbound WhatsApp messages (1:1 and groups, with media) are relayed into a Chatwoot API-channel inbox as incoming messages; agent replies (outgoing, non-private) are sent back to WhatsApp. Assigning a human agent in Chatwoot triggers handover — other OpenWA bots stop auto-replying on that chat until the conversation is unassigned. Chatwoot contacts are keyed on the WhatsApp JID, so matching is stable across WhatsApp's @lid migration, and a group maps to one synthetic contact with sender-prefixed messages. Inbound and outbound are serialized by a per-chat lock, so a cold-start burst can't create duplicate contacts or conversations; both directions are idempotent.

An agent's Reply to in Chatwoot is relayed as a real WhatsApp quote where the engine allows it. A text reply is quoted; if the quoted message has fallen outside the engine's retained window the engine refuses the quote and the reply is re-sent unquoted rather than failing. A reply carrying an attachment is never quoted — the engine's media path cannot quote at all.

Agent replies and conversation-status changes arrive on an account-level Chatwoot webhook that OpenWA verifies (HMAC) before the adapter sees it — agent-bot / inbox webhooks are unsigned and are not supported.

Configuration.

KeyRequiredDefaultDescription
baseUrlyesPublic https origin of your Chatwoot, e.g. https://app.chatwoot.com. Its host is auto-allowed through the manifest's net.allowConfigHosts. Origin only — a value carrying a path is rejected when the settings are saved.
apiTokenyesChatwoot agent-bot API token. Stored redacted.
accountIdyesNumeric Chatwoot account id.
inboxIdyesNumeric id of the API-channel inbox.
relayGroupsnotrueRelay group chats as well as 1:1.
relayMedianotrueUpload media from WhatsApp as Chatwoot attachments.
relayOwnMessagesnotrueRelay messages the account itself sends from the linked phone.
backfillLimitno0On start, backfill at most this many recent chats per session.
backfillAllOncenofalseBackfill every chat once on first boot.
Mint the instance over the REST API, with the Chatwoot secret

The adapter verifies the webhook HMAC signature against the instance's ingress secret, so that secret must equal the secret Chatwoot shows on the webhook's edit form — and Chatwoot generates the secret itself, so it can only be copied into OpenWA. You need Chatwoot v4.12.0 or newer, the first release whose account-level webhooks carry a per-webhook secret and a timestamped signature (X-Chatwoot-Timestamp). Create the webhook at Integrations → Webhooks, subscribed to message_created and conversation_updated, then mint the instance with POST /api/integration/plugins/chatwoot-adapter/instances, passing the Chatwoot webhook secret as secret.

Do not mint from the dashboard: its instance form has no secret field and auto-generates a random one that can never match Chatwoot's — every agent reply then fails HMAC verification with a 401. The secret cannot be edited after minting; to change it, delete the instance and re-mint.

faq-bot

Auto-replies to inbound messages from configurable keyword or regex rules.

FieldValue
Identifierfaq-bot
Version0.2.1
Statusstable
Requires OpenWA≥ 0.6.1 (tested 0.14.0)
Typeextension
Permissionsmessages:send
Hooksmessage:received
RepositoryOpenWA-plugins/faq-bot

What it does. Matches each inbound message against an ordered list of rules. Each rule is contains, exact (both case-insensitive), or regex (compiled with the i flag). The first matching rule wins, and its reply is sent as a quoted reply. If nothing matches and fallbackReply is set, the fallback is sent, throttled per chat by fallbackCooldownSec. Group chats are ignored unless respondInGroups is set. An invalid regex rule is skipped with a warning; a structurally invalid rules value fails fast and shows as ERROR in the dashboard.

Configuration.

KeyRequiredDefaultDescription
rulesyesJSON array of { mode, pattern, reply } rules, where mode is contains, exact, or regex.
fallbackReplyno""Reply sent when no rule matches. Empty stays silent.
fallbackCooldownSecno600Minimum seconds between fallback replies to the same chat. 0 replies every time.
respondInGroupsnofalseWhether to reply in group chats.

Example rules:

[
{ "mode": "contains", "pattern": "harga", "reply": "Harga mulai Rp100.000. Ketik 'menu' untuk detail." },
{ "mode": "exact", "pattern": "menu", "reply": "1) Harga 2) Jam buka 3) Lokasi" },
{ "mode": "regex", "pattern": "^/start", "reply": "Selamat datang! Ada yang bisa kami bantu?" }
]

group-translate

Auto-translates group messages between participants' languages via a LibreTranslate backend, controlled in-chat with /tr commands.

FieldValue
Identifiergroup-translate
Version1.3.1
Statusstable
Requires OpenWA≥ 0.7.0 (tested 0.14.0)
Typeextension
Permissionsmessages:send, engine:read, net:fetch, storage:use
Hooksmessage:received
RepositoryOpenWA-plugins/group-translate

What it does. Learns each group member's language from what they type (or pins it with /tr setlang), then posts a combined reply translating each message into the other languages present. Everything is managed in-chat with /tr commands: read-only commands are open to anyone; state-changing commands are admin-gated, resolved via ctx.engine.getGroupInfo (hence engine:read). Because WhatsApp delivers a group message's author under a privacy id (…@lid) while the participant list comes back under phone ids, an admin who fails the direct comparison is re-resolved to their phone identity through the host and compared again, so admin commands work across the @lid/phone split. Translation is disabled until an admin runs /tr on. Outbound translate calls go through the host's SSRF-guarded ctx.net.fetch. A per-call timeout plus a circuit breaker back off a slow or flaky backend instead of stalling the chat.

Which backends are reachable without repackaging

Since v1.3.0 the manifest ships net.allow for localhost and 127.0.0.1 — loopback on any port — and declares libretranslateUrl in net.allowConfigHosts, which admits an operator-configured host over https only. So a loopback backend and a public https backend both work as shipped. The one shape still needing a manifest edit and a re-package is a plain-http backend on a non-loopback host (for example http://libretranslate:5000 inside a Docker network) — add its host:port to net.allow, or put it behind https.

The host-level loopback guard is separate and applies regardless: a localhost/127.0.0.1 (or other private) address still needs SSRF_ALLOWED_HOSTS set on the OpenWA host, because the SSRF guard blocks those by default. The default libretranslateUrl is a loopback address, so out of the box the plugin cannot reach its backend until that is set.

Configuration.

KeyRequiredDefaultDescription
libretranslateUrlyeshttp://localhost:7001Base URL of your LibreTranslate instance. Loopback on any port works as shipped; any other host is admitted over https only. A loopback or private address also needs SSRF_ALLOWED_HOSTS on the gateway.
libretranslateApiKeynoSecret API key, if your instance requires one. Redacted on read.
timeoutMsno4000Per-call timeout. Keep at or below the host hook budget (5000 ms).
commandPrefixno/trThe in-chat command prefix.
minLengthno2Minimum message length to translate.
maxLengthno2000Maximum message length to translate.
denyReplynofalseReply "admins only" when a non-admin runs a restricted command.
announceInGroupsnofalsePost the bot's introduction the first time it sees a message in a group. Off by default — one enable would otherwise announce the bot into every group the account belongs to. /tr help prints the same text on demand.

In-chat commands (default prefix /tr):

CommandWhoEffect
/tr helpanyoneShow the command list.
/tr statusanyoneShow whether translation is on and per-participant languages.
/tr on · /tr offadminEnable or disable translation in this group.
/tr setlang <code> [@user]adminPin a participant's language.
/tr auto [@user]adminResume auto-learning a participant's language.
/tr ignore · /tr unignore [@user]adminSkip or resume translating a participant.
/tr grant · /tr revoke [@user]adminDelegate or remove control to a non-admin participant.

gsheets-logger

Logs every WhatsApp message event to a Google Sheet via a service account — an append-only audit trail across all sessions.

FieldValue
Identifiergsheets-logger
Version0.3.3
Statusstable
Requires OpenWA≥ 0.7.0 (tested 0.14.0)
Typeextension
Permissionsnet:fetch, storage:use
Hooksmessage:received, message:sent, message:failed, message:ack
RepositoryOpenWA-plugins/gsheets-logger

What it does. Writes one row per message event — across message:received, message:sent, message:failed, and message:ack — to a Google Sheet, using a fixed 14-column schema. It authenticates as a Google service account (JWT RS256) with no runtime dependencies. Writes are buffered and flushed in batches, with retain-on-failure (rows are kept and retried on a Sheets error), a 5000-row cap, and persistence to plugin storage so the buffer survives restarts. net:fetch is the only permission it declares, and its net.allow names just the two hosts it ever calls — oauth2.googleapis.com and sheets.googleapis.com. It never sends messages and never reads contacts.

The 14 columns, one row per event:

timestamp | sessionId | event | direction | chatId | from | to | senderName | isGroup | type | body | messageId | ackStatus | error
Setup requires two separate authorizations

Enabling the Google Sheets API on the project and sharing the spreadsheet with the service account (as Editor) are independent steps. Skipping either fails with a different 403 in the logs. See the plugin's README for the full setup walkthrough.

Configuration.

KeyRequiredDefaultDescription
serviceAccountJsonyesFull service-account key JSON. Stored as a secret. Share the sheet with this account's client_email as Editor.
spreadsheetIdyesThe ID from the sheet URL, between /d/ and /edit.
sheetTabnoLogsTarget tab name. The tab must already exist, with a header row of your choosing — the plugin appends data rows only.
flushIntervalSecno5Seconds between flushes.
flushBatchSizeno20Flush early once this many rows are buffered.
message:ack rows

The message:ack rows fill the messageId and ackStatus columns and require OpenWA ≥ 0.6.1; older builds never emitted the hook.

http-action

Trigger safe REST API requests from WhatsApp commands and map JSON responses back to chat — connect WhatsApp to an existing HTTPS API without webhook middleware.

FieldValue
Identifierhttp-action
Version0.2.2
Statusstable
Requires OpenWA≥ 0.8.0 (tested 0.14.0)
Typeextension
Permissionsnet:fetch, conversation:send, storage:use
Hooksmessage:received
RepositoryOpenWA-plugins/http-action

What it does. Each inbound message is matched against an ordered list of actions; the first matching action wins, and its trailing arguments are parsed (a double-quoted run is kept as one argument). A matched action issues a fixed-origin GET or JSON POST to a single HTTPS baseUrl — the path is server-relative and every interpolated segment is URL-encoded, so a message can never change the host or inject a path segment. The JSON response is rendered into a reply via {{response.field}} templates, with separate notFoundTemplate and errorTemplate. Direct chats are the default; group chats are ignored unless respondInGroups is set. One request runs per message, idempotent across WhatsApp redelivery (storage-backed dedup), with a per-chat cooldown and an off-dispatch handler so a slow upstream never stalls the inbound hook.

Configuration.

KeyRequiredDefaultDescription
baseUrlyesSingle HTTPS origin every request targets. Auto-added to net.allow via allowConfigHosts.
actionsyesJSON array of action objects (shape below).
authTypenononenone, bearer, or apikey.
authTokencond.Bearer token or API key, per authType. Stored redacted.
apiKeyHeadercond.X-API-KeyHeader name when authType is apikey.
respondInGroupsnofalseRun in group chats.
timeoutMsno3000Per-request timeout. Min 500.
cooldownSecondsno3Per-chat cooldown between runs of the same action.

Each action is { id, match: { type, value, caseSensitive? }, request: { method, path, query?, headers?, bodyTemplate? }, replyTemplate, notFoundTemplate?, errorTemplate? }. match.type is exact or prefix. Template variables: args.0, args.1, … plus message.id, message.body, chat.id, sender.id, sender.phone, sender.name, session.id, and response.<path> (the parsed JSON body).

supabase-otp-hook

Deliver Supabase Auth phone OTPs over WhatsApp. Supabase's Send SMS hook (Standard Webhooks-signed) is verified host-side, and the plugin sends the OTP via an OpenWA WhatsApp session — with synchronous feedback to Supabase.

FieldValue
Identifiersupabase-otp-hook
Version0.3.0
Statusbeta
Requires OpenWA≥ 0.8.16 (tested 0.14.0)
Typeextension
Permissionswebhook:ingress, messages:send
Hooks— (ingress-driven)
RepositoryOpenWA-plugins/supabase-otp-hook

What it does. Supabase calls the plugin's OpenWA ingress URL. The host verifies the Standard Webhooks signature against the instance secret (webhook-id / webhook-timestamp / webhook-signature, base64 HMAC-SHA256, constant-time, 5-minute replay window) — a bad signature returns 401 before the plugin runs. The host then runs a session-alive preflight that returns 503 on a dead WhatsApp session, and fast-acks Supabase with 200 on accept. The plugin parses { user: { phone }, sms: { otp } }, normalizes the phone to <digits>@c.us, and fires the WhatsApp send in the background (bounded to 5 s) so a timeout-induced retry can't duplicate the OTP. Events are ordered per user.id and deduped on webhook-id. Since v0.3.0 the plugin no longer declares engine:read: the canonicalChatId round-trip it was used for could only ever return the same <digits>@c.us string, so it was dropped along with its 2-second race on the OTP critical path.

This is the canonical example of a plugin that owns an inbound URL rather than a chat hook: the webhook:ingress capability exposes the verified endpoint, and verification happens in the host (not the plugin) so a signature failure never reaches plugin code.

Configuration.

KeyRequiredDefaultDescription
appNameyesApplication name interpolated into the OTP message via {appName}.
messageTemplateno{appName} | Your verification code is {otp}Message body. Supports {appName} and {otp} placeholders.
fallbackSessionIdnoSession to send through if the request doesn't name one.
debugnofalseVerbose logging.
Standard Webhooks secret

The instance secret you set in OpenWA is the same secret Supabase generates when you register the webhook URL — the host uses it to verify every delivery. OpenWA v0.8.16+ is required for the standard-webhooks signature scheme and the ingress preflight/response contract.

The dead-session 503 only holds when the instance is bound to a session

The host's preflight probes the instance's sessionScope. Bind the instance to a logged-in session and a dead session is reported to Supabase as 503, so the provider retries. Leave sessionScope blank and rely on the fallbackSessionId plugin config instead, and the preflight is skipped: a delivery whose fallback session is down is acked 200 and lost, with no provider retry and only a sendText failed (background) log line. Bind the instance wherever the sending session is known.

typebot-connector

Run a Typebot flow as the brain of a WhatsApp bot — inbound messages drive a Typebot chat session, and the bot's text, media, and numbered-choice replies come back to WhatsApp.

FieldValue
Identifiertypebot-connector
Version0.2.2
Statusstable
Requires OpenWA≥ 0.8.2 (tested 0.14.0)
Typeextension
Permissionsnet:fetch, conversation:send, storage:use
Hooksmessage:received
RepositoryOpenWA-plugins/typebot-connector

What it does. On each inbound message in scope, the plugin resumes (or starts) the contact's Typebot session over the live Chat API and renders the bot's reply bubbles back into WhatsApp. Text bubbles are converted to WhatsApp formatting; image, video, and audio bubbles are sent as media; a choice step is shown as a numbered list and the contact's numeric reply is mapped back to the option. Typed inputs (email, number, date, …) are re-asked on a bad value, and a file-input step accepts a photo or file the contact sends. The session resets when the flow ends or after the idle timeout, so the next message starts fresh. It runs sandboxed in the plugin worker and polls Typebot's Chat API over the host's SSRF-guarded ctx.net.fetch — no public URL or webhook is required. Typebot holds the session state; the plugin persists only the session id and the bot's expected next input.

Configuration.

KeyRequiredDefaultDescription
apiHostyeshttps://typebot.ioTypebot API host. https://typebot.io for Cloud, or your self-hosted URL. Auto-added to net.allow.
publicIdyesTypebot public ID from the bot's Share settings.
apiTokennoAPI token, only for a restricted/preview bot. Stored redacted.
respondInGroupsnotrueRun in group chats.
sessionTimeoutMinutesno30Idle minutes before the session resets.
passContactVariablesnotruePass waNumber, waName, waChatId as Typebot flow variables.
mediaHostnoHost serving Typebot media bubbles (when separate from apiHost). Auto-added to net.allow.

voice-transcription

Transcribes inbound voice notes to text via an OpenAI-compatible speech-to-text backend and delivers a message.transcription event to your webhook.

FieldValue
Identifiervoice-transcription
Version1.2.3
Statusbeta
Requires OpenWA≥ 0.7.0 (tested 0.14.0)
Typeextension
Permissionsnet:fetch, messages:send, storage:use
Hooksmessage:received
RepositoryOpenWA-plugins/voice-transcription

What it does. On each inbound voice note, runs speech-to-text against any OpenAI-compatible /v1/audio/transcriptions endpoint (self-hosted Speaches/faster-whisper, or hosted Groq/OpenAI) and POSTs a message.transcription event to your delivery webhook — so a bot or AI can read and reply to audio. Transcription runs off the message-delivery path as an un-awaited task: it never touches the message.received payload, never blocks delivery, and is not bound by the 5-second hook budget. It delivers completed (with the transcript), failed, or skipped status, so a consumer always knows a voice note arrived even when it can't be read. The plugin is in beta and disabled until enabled.

Treat the transcript as untrusted

transcription.text is attacker-controlled speech; the event marks it untrusted: true. A downstream LLM responder must place it in a user role, never a system or trusted context — a caller can speak injection instructions a typist never would.

Delivered event (POSTed to deliveryWebhookUrl):

{
"event": "message.transcription",
"sessionId": "…",
"messageId": "<waMessageId>",
"chatId": "…@s.whatsapp.net",
"status": "completed",
"source": "speech-to-text",
"untrusted": true,
"transcription": { "text": "…", "language": "es", "provider": "faster-whisper", "model": "small" }
}

Correlate it to the original voice note by messageId. It arrives shortly after message.received, out of order — do not assume ordering. When deliverySecret is set, the body is HMAC-SHA256 signed in X-OpenWA-Signature: sha256=<hex>, the same scheme as core webhooks.

Configuration.

KeyRequiredDefaultDescription
sttBaseUrlyesOpenAI-compatible STT base URL (/v1/audio/transcriptions is appended). An https host is admitted automatically; a localhost target also needs SSRF_ALLOWED_HOSTS on the host.
sttApiKeynoBearer key for a hosted backend (Groq/OpenAI). Blank for a local Speaches instance. Stored redacted.
modelnosmallWhisper model name, e.g. small, base, whisper-large-v3-turbo.
languageno(auto)Optional BCP-47 hint. Blank auto-detects.
providernofaster-whisperInformational label recorded in the delivered event.
timeoutMsno20000Per-request STT timeout. Min 1000, max 30000.
enabledMessageTypesno["voice"]Message types to transcribe. Add audio to also transcribe non-PTT audio (more cost).
maxSizeBytesno16777216Skip audio larger than this.
maxPerHourno60Best-effort per-session hourly transcription cap.
deliveryWebhookUrlcond.Endpoint receiving the event. An https host is admitted automatically. Optional if you only use chatDelivery.
deliverySecretnoHMAC-SHA256 signs the body in X-OpenWA-Signature. Stored redacted.
deliveryTimeoutMsno5000Delivery POST timeout. Min 1000, max 30000.
chatDeliverynooffAlso post the transcript into WhatsApp: off (webhook only), self (a note to your own number), or reply (quote-reply to the sender).

This plugin ships net.allow for localhost, 127.0.0.1, api.groq.com:443, and api.openai.com:443, and since v1.1.0 also declares sttBaseUrl and deliveryWebhookUrl in net.allowConfigHosts — so any https STT or delivery URL you configure is admitted automatically, with no manifest edit and no re-package. The static entries are what still covers a plain-http local backend. For a deeper how-to, see Voice Transcription.

Per-session config support

A sessionScoped plugin (the default) can carry per-session config overrides — two WhatsApp sessions under one plugin instance can run different settings. A plugin honors an override only if it re-reads ctx.config inside its hook rather than using a snapshot cached at enable. The status per plugin:

PluginPer-session configNotes
after-hours✅ SupportedAll fields per session; takes effect on next message.
chat-flow✅ SupportedAll fields per session; flow state is per (session, chat).
chatwoot-adapter✅ SupportedAll fields per session — first-class multi-tenant shape.
faq-bot✅ SupportedAll fields per session (different rule sets per number).
group-translate⚠️ Supported, with caveatConfig-signature caching; multi-backend isolation needs one instance per session.
gsheets-logger❌ Not supportedSingle-buffer single-sink design; use one instance per session.
http-action✅ SupportedAll fields per session (different endpoints/action sets).
supabase-otp-hook✅ SupportedAll fields per session; applies to the instance's bound session.
typebot-connector✅ SupportedAll fields per session; flow state is per (session, chat).
voice-transcription⚠️ Supported, with caveatConfig-signature caching; multi-backend isolation needs one instance per session.

For how to write a plugin that honors overrides, see Building a Plugin.

Next steps