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

  1. 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 fieldWhere 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
  2. 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 fieldWhere 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
  3. 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 fieldWhere 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
  4. 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 fieldWhere it comes from
    arguments.bodyNon-empty, trimmed, capped in length
    arguments.toMust equal event.chat_id, or the call is dropped and logged
  5. 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 fieldWhere it comes from
    AuthorizationBearer wpt_… — held by the relay, never sent to the model
    bodyarguments.body
    toarguments.to — the validated chat id
    typing_time4
  6. 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 fieldWhere 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

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

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.