Set up Meta's AI agent on a WhatsApp number through the API, teach it about the business, test it, roll it out and hand chats back to people.
Meta offers its own AI agent for WhatsApp Business numbers. When it is on, it answers customers in the chat using the business information, FAQs, websites and files you give it, follows the tone and rules you write as skills, and can call your APIs to look things up. Pozy exposes the whole setup under /v1/accounts/{accountId}/business-agent, so you can configure the agent for a merchant without them opening Meta's tools, except for two steps Meta keeps for the merchant.
#What only the merchant can do
| Step | Where | What happens until it is done |
|---|---|---|
| Accept Meta's Business Agent terms | The Business Agent tab in WhatsApp Manager | Every agent call returns business_agent_terms_not_accepted |
| Attach a payment method to the Business Agent billable account | Meta's Billing Hub | The agent can be switched on, but Meta delivers none of its replies to the public. Testing with an allowlist still works. |
GET /v1/accounts/{accountId}/business-agent reports the first under manualSteps (with the URL to send the merchant to). Meta does not expose billing state, so the second is listed under unverifiedSteps once an agent exists and you decide when to stop showing it.
#Which numbers qualify
The number must be on the Cloud API, in a vertical and country Meta supports, in good standing, and not running another AI product. Meta allows one AI product per number, so a coexistence number that still has the WhatsApp Business app's own AI linked is not eligible until the merchant unlinks it in the app. eligible on the status read is Meta's answer, and creating an agent on an ineligible number returns 403 business_agent_not_eligible.
#Set it up
#Check where the merchant stands
curl https://api.pozy.ai/v1/accounts/6712a9d4c0e3b58f1a7d2e64/business-agent \
-H "Authorization: Bearer $POZY_API_KEY"{
"eligible": true,
"termsAccepted": true,
"onboarded": false,
"enabled": false,
"agentId": null,
"settings": null,
"manualSteps": [],
"unverifiedSteps": []
}#Create the agent
Call POST .../business-agent/onboard once. Meta then prepares the agent, which takes about a minute. Configuration calls made before it is ready return business_agent_not_found, so wait and retry.
#Give it knowledge
Everything the agent says comes from what you load here.
| What | Call | Notes |
|---|---|---|
| Business information | PUT .../business-information |
Payment methods, return policy, how to buy, shipping, description, email, hours and address. A full replace: fields you leave out are cleared. |
| FAQs | POST .../faqs |
One specific question per entry (question up to 1,000 characters, answer up to 8,000). Answer quality drops past a few hundred entries. |
| Websites | POST .../websites |
Meta crawls the site and recrawls it from time to time. Narrow it with included and excluded subdomains and URL patterns. Check crawl_status and crawl_error. |
| Files | POST .../files |
PDF, DOC, DOCX, PNG and JPG (CSV and XLSX when Meta enables it for the account), up to 100 MB |
None of these create calls are idempotent. A retry adds a second entry, so check the list before retrying after a timeout.
curl -X POST https://api.pozy.ai/v1/accounts/6712a9d4c0e3b58f1a7d2e64/business-agent/faqs \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"question": "Can I order a cake for the same day?",
"answer": "Same-day cakes are available until 11am for pickup after 4pm. For delivery, order a day ahead."
}'const accountId = "6712a9d4c0e3b58f1a7d2e64";
const res = await fetch(`https://api.pozy.ai/v1/accounts/${accountId}/business-agent/faqs`, {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.POZY_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
question: "Can I order a cake for the same day?",
answer: "Same-day cakes are available until 11am for pickup after 4pm. For delivery, order a day ahead.",
}),
});
if (!res.ok) throw new Error((await res.json()).error);import os
import requests
H = {"Authorization": f"Bearer {os.environ['POZY_API_KEY']}"}
base = "https://api.pozy.ai/v1/accounts/6712a9d4c0e3b58f1a7d2e64/business-agent"
requests.post(f"{base}/faqs", headers=H, timeout=30, json={
"question": "Can I order a cake for the same day?",
"answer": "Same-day cakes are available until 11am for pickup after 4pm. For delivery, order a day ahead.",
}).raise_for_status()#Give it a voice
- Skills (
POST .../skills) are written instructions in the brand's voice: how to greet, what to never promise, when to suggest a product. Each skill is up to 20,000 characters and readspending_reviewuntil Meta's content review passes it. - UI skills (
POST .../ui-skills) tell the agent when to send a rich component (a link button, image, carousel, list, reply buttons, location or a Flow) and what to put in it. - Never-say phrases live in the settings (
never_say_phrases) and replace the whole list on each update.
#Connect your systems (optional)
A connector lets the agent call your API mid-conversation, for example to check an order status. Create it with a base_url and an auth_type (OAUTH2_CLIENT_CREDENTIALS, API_KEY or NONE) using POST .../connectors, then define each operation as a tool with POST .../connectors/{connectorId}/tools.
Run a tool once with .../run to check it before the agent relies on it. Rotate secrets with .../credentials. For an MCP server connector, .../refresh-tools rediscovers its tools. .../logs shows failures from the last 7 days.
#Test it privately
POST .../test-messages runs a message through the full agent in Meta's sandbox. No customer is involved and no tokens are billed. Pass the returned conversation_id back to continue the same thread. Meta limits how many test messages a number can send per hour.
curl -X POST https://api.pozy.ai/v1/accounts/6712a9d4c0e3b58f1a7d2e64/business-agent/test-messages \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "message": "Do you deliver to Cascais on Sundays?" }'The response holds agent_response, plus handoff_reason or no_response_reason when the agent chose not to answer, and any quick_replies it offered.
For a broader check, POST .../evals simulates a set of scenarios and scores them. It answers 202 with a job id to poll with GET .../evals.
#Roll it out
Turn the agent on with PATCH .../settings. Start with a few real numbers on an allowlist, then open it to everyone.
# 1. Allow a test phone
curl -X POST https://api.pozy.ai/v1/accounts/6712a9d4c0e3b58f1a7d2e64/business-agent/allowlist \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "consumer_phone_number": "+351912345678" }'
# 2. Switch on for allowlisted people only
curl -X PATCH https://api.pozy.ai/v1/accounts/6712a9d4c0e3b58f1a7d2e64/business-agent/settings \
-H "Authorization: Bearer $POZY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"rollout": { "enabled": true },
"ai_audience": "ALLOWLISTED_ONLY",
"handoff": { "enabled": true, "message_selection": "CUSTOM", "message": "Let me get someone from the shop to help you." }
}'When you are happy, set ai_audience to EVERYONE. That needs the payment method from the merchant steps above.
#Settings
| Setting | Effect |
|---|---|
rollout.enabled |
true turns the agent on for new conversations. false stops it on every thread. |
ai_audience |
ALLOWLISTED_ONLY or EVERYONE |
handoff |
What the agent says when it passes a chat to a person (DEFAULT, AGENT or CUSTOM message, up to 1,024 characters) |
followup |
A nudge sent after a quiet period: 5 minutes to 24 hours, or 0 for off |
never_say_phrases |
Phrases the agent must never use |
Settings update partially: fields you leave out keep their value.
#Work alongside the agent
While the agent holds a conversation, Pozy still sees everything:
- Customer messages arrive as
message.receivedwithmetadata.standby: true. - The agent's replies arrive as
message.sentwithsource: "meta_business_agent".
Sending any message from Pozy takes the conversation back from the agent. If you run your own bot on the same number, skip messages marked standby or you will interrupt the agent on every turn.
The agent also hands over by itself when it is unsure or the customer asks for a person. To give a conversation back to the agent without sending anything, call POST /v1/inbox/conversations/{conversationId}/thread-control with action: "release". Every change of control fires conversation.control_changed.
#Tell the agent about events
When something happens in your systems that the customer should hear about (an order shipped, a document approved), send it with POST .../business-agent/events: the customer's number in to, a type, a description and a payload. The agent writes the message. The customer must already have a conversation with the number. The call answers 202 with an event id; check the outcome with GET .../events/{eventId}.
#Costs
Meta bills agent replies by tokens on the merchant's Business Agent billable account, separately from Pozy. Meta's own figure is roughly 4 to 5 US cents per message. Cap the spend with PUT .../budget: each budget is a max_budget of token or ai_turn units over one_day, seven_days, fourteen_days or thirty_days. Sending the list replaces all budgets, and an empty list means no cap. When a cap is hit the agent finishes its turn, stops answering and hands the thread to a person until the window rolls over.
#Endpoints
#Webhook events
| Event | When it fires |
|---|---|
message.received |
Message received. |
conversation.control_changed |
Conversation control changed. |
message.sent |
Message sent. |