Build on SeoBotPages
Two ways in. A personal key if you are automating your own business, and an app if you are building something other businesses here will install. Both end up calling the same API.
Quick start
Automating your own business takes one key and one request. Create a key in your dashboard under AI assistant, then:
curl -X POST https://verifiedbizz.com/mcp \
-H "Authorization: Bearer vb_your_key" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"list_my_businesses","arguments":{}}}'
That is the whole API surface: one endpoint, JSON-RPC 2.0, and the tools
listed below. tools/list returns
the same schemas this page is generated from.
Authentication
Every call carries Authorization: Bearer <token>.
There are two kinds and the API treats them identically:
| Prefix | What it is | Who makes it |
|---|---|---|
vb_ |
Personal key | A seller, for their own businesses |
vbt_ |
App token | A seller allowing your app, on one business |
Without a token the API still answers, but only the public tools — search_businesses, get_business, list_products, check_coverage, find_suppliers — and only about published businesses.
A token never grants more than the person behind it already had. Every write is checked against that account's role on that business, the same check the dashboard makes. There is no key that can create an account or a business.
Connecting a seller
Standard OAuth 2.0 authorization code flow with PKCE. Register your app in the dashboard under Connected apps › Build an app to get a client id and secret.
1. GET https://verifiedbizz.com/apps/authorize
?client_id=vba_...
&redirect_uri=https://yourapp.example/callback
&response_type=code
&scope=read+events
&state=<random>
&code_challenge=<base64url(sha256(verifier))>
&code_challenge_method=S256
The seller picks ONE business and presses Allow.
2. GET https://yourapp.example/callback?code=...&state=...
3. POST https://verifiedbizz.com/apps/token
grant_type=authorization_code
&code=...
&redirect_uri=https://yourapp.example/callback
&client_id=vba_...
&client_secret=vbs_...
&code_verifier=<verifier>
→ {"access_token":"vbt_...","token_type":"Bearer",
"scope":"read,events",
"brand":{"slug":"acme-tools","name":"Acme Tools"}} The token comes back already pinned to the business the seller chose, so there is no lookup call before your first useful one — and no way to reach another business on their account.
Three things that are not optional
- state — mint it, store it, compare it. Without it somebody can hand your user a callback URL of their choosing and connect their business to your user's account.
- PKCE — without it, a code seen in a log, a referrer header or a browser history is enough for somebody else to redeem.
- Exact redirect — we compare the whole string. A mismatch is refused on our page, never redirected, so a mistake here is visible rather than exploitable.
There is no refresh token and no expiry. A token lasts until the
seller revokes it in their dashboard, at which point the very next call
answers 401. That is the only
notice you get, so handle it by asking them to connect again.
Scopes
Ask for the least you need — every one you request is a line on the seller's consent screen, and your app cannot ask for more than it registered for.
| Scope | What the seller is told |
|---|---|
read |
Read your listing, products, services and reviews |
write |
Add and edit your products, services and pages |
leads |
Read your enquiries, orders and customer details |
messages |
Read your WhatsApp conversations and send replies |
events |
Be notified when something happens in your business |
events is what lets you register a webhook and be told things
without asking. See Events.
Tools
Called with tools/call. Everything
below is generated from the server's own definitions, so it cannot drift from
what the API accepts. Bold parameters are required.
search_businesses
public
Search verified business listings by what they sell and where. Use for "who sells X", "X suppliers in Y", or any question needing a real company that trades in a place. Returns names, trades, cities and page URLs. Pass place as a city, district or state name.
-
querystring - What they sell, or a company name. e.g. "packaging", "biscuit manufacturer".
-
placestring - City, district or state to search in. Optional.
-
limitinteger - Maximum results, 1-50. Default 10.
get_business
public
Full record for one business: contact details, GST registration, the complete catalogue with prices and minimum order quantities, areas served and FAQs. Use after search_businesses, or when the slug is already known. The slug is the last part of its page URL.
-
slugstring - Business slug, e.g. "nandi-packaging-works".
list_products
public
Just the catalogue of one business: product names, prices, units, minimum order quantities, GST rates and stock. Lighter than get_business when the question is only about what they sell for how much.
-
slugstring - Business slug.
-
kindstring - Default "both".
check_coverage
public
Does this business deliver to or operate in this place? Answers yes or no with the reason — based there, or covering it as a service area. Use before telling somebody a supplier can serve them.
-
slugstring - Business slug.
-
placestring - City, district or state name.
find_suppliers
public
Every business in one trade that serves one place, including those based elsewhere that cover it. Use when the trade is known and a shortlist is wanted — "packaging suppliers in Bengaluru". For a vaguer question, search_businesses is the better tool.
-
categorystring - Trade, e.g. "packaging suppliers".
-
placestring - City, district or state name.
-
limitinteger - Maximum results, 1-50. Default 20.
list_my_businesses
needs a token
The businesses this key can act on, with the role held on each. Call first: every other authenticated tool takes one of these slugs.
No parameters.
update_business
needs a token
Change the listing text and contact details of one business. Only the fields passed are written; anything omitted is left alone. Does not change the trade, the location or verification status — those are checked by hand.
-
slugstring - Business slug.
-
descriptionstring - The About text. Plain text or simple HTML.
-
taglinestring - One short line under the name.
-
phonestring - Public phone number, digits and country code.
-
whatsappstring - WhatsApp number if it differs from the phone.
-
emailstring - Public enquiry address.
-
websitestring - Full URL including https://.
-
addressstring - Street address. The city comes from the listing and is not set here.
create_product
needs a token
Add a product to a business catalogue. Price is per unit and excludes GST unless the business lists tax-inclusive prices. Returns the page URL of the new product.
-
slugstring - Business slug.
-
namestring - Product name as a buyer would search for it.
-
pricenumber - Per unit. Omit for "price on request".
-
unitstring - e.g. "pcs", "kg", "box".
-
moqstring - Minimum order quantity, e.g. "10,000 pcs".
-
descriptionstring - What it is, what it is made of, who it suits.
-
gst_ratenumber - Percent. Defaults to the business rate.
update_product
needs a token
Change a product already in the catalogue, found by its slug. Only the fields passed are written. Set active=false to take it off the site without deleting it.
-
slugstring - Business slug.
-
product_slugstring - Product slug, from list_products.
-
namestring - Product name as a buyer would search for it.
-
pricenumber - Per unit. Set 0 for "price on request".
-
unitstring - e.g. "pcs", "kg", "box".
-
moqstring - Minimum order quantity, e.g. "10,000 pcs".
-
descriptionstring - What it is, what it is made of, who it suits.
-
stockinteger - Units available. 0 shows the product as out of stock.
-
activeboolean - false takes it off the site without deleting it.
list_orders
needs a token
Orders placed with one business, newest first, with customer, total and status. Use to answer "what came in today" or to find an order before answering a complaint about it.
-
slugstring - Business slug.
-
statusstring - Filter by status. Optional.
-
limitinteger - How many to return. Default 20, newest first.
list_tickets
needs a token
Complaints raised against one business: New, Assigned or Resolved. Every one is also copied to SeoBotPages. Use before replying to a customer so the history is known.
-
slugstring - Business slug.
-
statusstring - Optional filter.
-
limitinteger - How many to return. Default 20, most recent first.
resolve_ticket
needs a token
Mark a complaint resolved, recording what was done about it. The note is kept on the ticket and is visible to SeoBotPages. Only for a complaint that is genuinely settled.
-
slugstring - Business slug.
-
refstring - Ticket reference, from list_tickets.
-
resolutionstring - What was done. Required — a ticket closed with no reason is not resolved, it is ignored.
Events
The other direction: we tell you when something happens, without you asking. Register one webhook per connection with the app token you already hold — no setup page, no URL for the seller to paste.
POST https://verifiedbizz.com/apps/hooks
Authorization: Bearer vbt_...
Content-Type: application/json
{"url":"https://yourapp.example/hook","events":"*"}
→ {"ok":true,"secret":"vbw_...","events":["*"]}
GET https://verifiedbizz.com/apps/hooks # what is registered, and every event name
DELETE https://verifiedbizz.com/apps/hooks # stop delivery, without losing the connection
Pass "events" as
"*" or a comma-separated list.
The connection needs the events
scope, and each event also needs the scope in its own row below — a
connection without messages is
never sent WhatsApp events, whatever it subscribed to.
| Event | Scope | Sent when |
|---|---|---|
lead.created |
leads |
Somebody asked this business for a quote. |
order.placed |
leads |
A new order was placed. |
order.status |
leads |
An order moved to a different status. |
order.paid |
leads |
An order was paid for. |
wa.message |
messages |
A customer sent this business a WhatsApp message. |
wa.conversation |
messages |
A customer started a new WhatsApp conversation. |
review.created |
read |
A review was left for this business. |
ticket.created |
read |
A complaint was raised against this business. |
ticket.resolved |
read |
A complaint was marked resolved. |
product.created |
read |
A product was added to the catalogue. |
product.updated |
read |
A product was changed. |
business.updated |
read |
The listing details were changed. |
What arrives
POST https://yourapp.example/hook
X-VB-Event: order.placed
X-VB-Delivery: 81422
X-VB-Timestamp: 1789452065
X-VB-Signature: sha256=<hex>
{"event":"order.placed",
"created_at":"2026-09-15T17:14:45+05:30",
"brand_id":855,
"data":{"order_id":5512,"total":2480,"status":"new", ...}}
Verify every delivery
$calc = hash_hmac("sha256",
$_SERVER["HTTP_X_VB_TIMESTAMP"] . "." . file_get_contents("php://input"),
$secret);
if (!hash_equals("sha256=" . $calc, $_SERVER["HTTP_X_VB_SIGNATURE"])) {
http_response_code(403); exit;
} The timestamp is part of what is signed, so a body captured off the wire cannot be replayed at you later. Reject anything more than a few minutes old.
Four things you can rely on
- Once per event. Every delivery is keyed on the fact that
happened, so a retried request or a double-submitted form cannot
produce two. Still key on
X-VB-Deliveryif you replay. - The body is frozen at the moment it happened, not rebuilt when a retry runs — so a late delivery is still an accurate one.
- Retries: five attempts over about nine hours, at roughly 1 min, 5 min, 30 min, 2 h and 6 h. Answer 2xx quickly and do your work afterwards; we time out at 10 seconds.
- Twenty consecutive failures switches the hook off. Saving it again turns it back on and clears the count.
Redirects are not followed — a 302 goes to an address the seller never authorised, so it counts as a failure rather than a delivery.
Sending on WhatsApp
Two plain REST endpoints, for a business that has connected its own WhatsApp number. Both need a token with write access, and both send on that business's own number — never the shared SeoBotPages one.
POST https://verifiedbizz.com/wa/send # free-form; only within 24h of their last message
POST https://verifiedbizz.com/wa/template # approved template; any time
curl -X POST https://verifiedbizz.com/wa/send \
-H "Authorization: Bearer vb_your_key" \
-H "Content-Type: application/json" \
-d '{"brand":"acme-tools","to":"919812345678","text":"Your order has shipped."}'
# outside the 24-hour window:
-d '{"brand":"acme-tools","to":"919812345678",
"template":"order_update","language":"en_US"}' WhatsApp only carries a free-form message inside the 24 hours a customer opened by writing first. Outside it, a template approved in that business's own Meta account is the only thing that will send — which is why these are two endpoints and not one.
Errors
| Status | Means |
|---|---|
401 |
No token, or it was revoked. For an app token this is how you learn the seller disconnected you. |
403 |
The token cannot reach that business, or is read-only and you asked to write. |
409 |
The state is wrong for the request — no WhatsApp number connected, or the 24-hour window has closed. |
422 |
Something in the request is missing or malformed. The message says which. |
429 |
Too many requests. Wait a minute. |
502 |
An upstream service refused. The message is theirs, passed through unchanged. |
A failing tool is not an HTTP error: it returns
isError with a message, so a model
or a script can read it and retry.
Rules
- Act as a person, never anonymously. Every write is attributed to the account behind the token and checked against its role.
- One business per app token. A seller with three shops who installs you on one has installed you on one.
- Do not ask for scopes you will not use. The consent screen shows every one, and an app asking for more than it needs is an app sellers decline.
- Handle 401 by reconnecting. There is no notification when a seller revokes; a 401 is the notification.
- Rate limit: 120 requests a minute, per address.
- Your app stays a draft — installable only by you — until we review it for the directory. Build and test with it in that state.
A complete working example
examples/connect-app.php in the repository is an entire app in
one file — connect, consent, token exchange, reading a business, and
handling revocation — with no dependencies.