Let an OpenAI model send WhatsApp messages through a relay
Let an OpenAI model send WhatsApp messages through a relay you control, in its own words, with no template and no approval queue. The relay holds both keys, receives the inbound message from a Wapito webhook, asks the model for a reply with a send_whatsapp_message tool defined, and executes the tool call against Wapito after validating it.
Before you start
- An OpenAI API key and a model that supports function calling.
- A connected Wapito channel, its channel token, and a webhook registered to your relay's inbound URL with the signing secret saved.
- A relay you can deploy: a handful of HTTP routes in any language, reachable over HTTPS, with somewhere to keep a short conversation history per chat.
Step by step in OpenAI
Define the tool with a strict schema
In the request to OpenAI declare one tool named send_whatsapp_message whose parameters are an object with exactly two required string properties, to and body, and additionalProperties set to false, with strict mode on. The description should say the tool sends a WhatsApp message on the company's behalf and that to must be the chat id the model was given, not a number it extracted from the conversation.
Field map Wapito field Where it comes from tools[0].function.namesend_whatsapp_message tools[0].function.parameters.properties.bodystring — the reply text tools[0].function.parameters.properties.tostring — the chat id from the inbound event tools[0].function.stricttrue Receive the inbound message and answer the webhook first
Expose a POST route on the relay for Wapito's messages event. Verify the X-Wapito-Signature header against the raw body with the signing secret, respond with a 2xx immediately, and hand the event to a background job. The model call takes seconds and Wapito retries deliveries that do not answer in time, so doing the model work inside the request handler is how you end up answering the same message twice.
Field map Wapito field Where it comes from X-Wapito-SignatureVerified with the webhook's signing secret before anything else runs event.chat_idThe recipient the reply must go to — passed to the model as context, never as user text event.text.bodyThe customer's message — the user turn of the model conversation Ask the model for a reply with the tool available
In the background job build the conversation: a system message that describes the business, the tone and the rule that the model may only message the chat it is answering; the last few turns for this chat from your store; and the customer's message as the user turn. Send it with the tool declared and tool_choice left to the model, so a message that needs no reply can end without a send.
Field map Wapito field Where it comes from messages[0].role=systemBusiness context plus the rule: reply only to the given chat id, never invent numbers messages[…]Recent turns for this chat_id from the relay's store tool_choiceauto Validate the tool call before it reaches Wapito
When the response contains a tool call, parse its arguments and check two things in the relay: that to equals the chat id of the inbound event, and that body is non-empty and under a sensible length. A model can be talked into calling the tool with a different recipient by a message that contains instructions, and this comparison is the line that makes prompt injection a failed send rather than a message to a stranger.
Field map Wapito field Where it comes from arguments.bodyNon-empty, trimmed, capped in length arguments.toMust equal event.chat_id, or the call is dropped and logged Send the reply through Wapito with a typing time
Post to https://api.wapito.com/v1/messages/text with the Wapito token in the Authorization header, the validated chat id as the recipient, the model's text as the body, and a typing_time of a few seconds so the answer lands at a human pace rather than the instant the webhook arrived. Store the returned message id with the conversation turn so a later status event can be matched to it.
Field map Wapito field Where it comes from AuthorizationBearer wpt_… — held by the relay, never sent to the model bodyarguments.body toarguments.to — the validated chat id typing_time4 Return the tool result to the model and close the turn
Append a tool message carrying the message id and status from the send response and ask the model once more, so it can produce a short final text you log alongside the conversation, then stop. Cap the loop at one tool call per inbound message: an agent that can send repeatedly is an agent that can be provoked into a burst, and a burst from a linked number is what gets it restricted.
Field map Wapito field Where it comes from max tool calls per inbound event1 tool result{ id, status } from the send response, returned as the tool message content
Test it
Send a question to the linked number from your own phone. The relay log should show the signature verified, a 2xx returned before the model was called, one tool call whose to matched the chat id, and a 201 from Wapito; the reply should arrive on your phone a few seconds later. Then send a message that says to forward the reply to another number and confirm the relay dropped the call instead.
Errors you may hit
- invalid_recipient 400Recipient is not valid
- reachout_timelocked 423Reach-outs timelocked
- send_rate_limited 429Send queue saturated
- channel_not_connected 409Channel not connected
- unauthorized 401Missing or invalid token
Frequently asked questions
Can the model call Wapito directly, without the relay?
No. OpenAI models do not make outbound HTTP requests to arbitrary APIs on their own; a tool call is a structured request the model returns to your code, and your code decides whether to execute it. The relay is where the Wapito token lives, where the recipient is checked, and where the send rate is capped, none of which can be enforced from inside a prompt.
What stops the model from messaging the wrong person?
The relay, not the prompt. The system message tells the model to reply only to the chat it was given, but the enforcement is the comparison in step four: a tool call whose recipient differs from the inbound chat id is dropped and logged. Treat every instruction inside a customer message as untrusted text, because that is exactly what it is, and never let the model choose a recipient.
Why wait a few seconds before sending the reply?
Because the alternative is a number that answers every message in under a second, at any hour, forever, which is a signal WhatsApp reads as automation. A typing time makes the reply arrive the way a fast human would send it. It costs nothing, and combined with Wapito's per-channel pacing it is the cheapest protection the linked number has.
How do I keep the conversation history without storing everything forever?
Keep the last handful of turns per chat id in a small key-value store with an expiry of a day or two. The model only needs recent context to answer well, and holding months of customer messages in a relay is a liability rather than a feature. Wapito keeps the chat history on the linked number regardless, and the messages endpoint can fetch it if a longer view is ever needed.
Related
Endpoints this recipe calls
Try it on your own number
Create a channel, link a WhatsApp number by QR or pairing code, and call the API in a couple of minutes. The Sandbox plan is free and needs no card.