Skip to content
SeoBotPages

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.

query string
What they sell, or a company name. e.g. "packaging", "biscuit manufacturer".
place string
City, district or state to search in. Optional.
limit integer
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.

slug string
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.

slug string
Business slug.
kind string
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.

slug string
Business slug.
place string
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.

category string
Trade, e.g. "packaging suppliers".
place string
City, district or state name.
limit integer
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.

slug string
Business slug.
description string
The About text. Plain text or simple HTML.
tagline string
One short line under the name.
phone string
Public phone number, digits and country code.
whatsapp string
WhatsApp number if it differs from the phone.
email string
Public enquiry address.
website string
Full URL including https://.
address string
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.

slug string
Business slug.
name string
Product name as a buyer would search for it.
price number
Per unit. Omit for "price on request".
unit string
e.g. "pcs", "kg", "box".
moq string
Minimum order quantity, e.g. "10,000 pcs".
description string
What it is, what it is made of, who it suits.
gst_rate number
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.

slug string
Business slug.
product_slug string
Product slug, from list_products.
name string
Product name as a buyer would search for it.
price number
Per unit. Set 0 for "price on request".
unit string
e.g. "pcs", "kg", "box".
moq string
Minimum order quantity, e.g. "10,000 pcs".
description string
What it is, what it is made of, who it suits.
stock integer
Units available. 0 shows the product as out of stock.
active boolean
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.

slug string
Business slug.
status string
Filter by status. Optional.
limit integer
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.

slug string
Business slug.
status string
Optional filter.
limit integer
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.

slug string
Business slug.
ref string
Ticket reference, from list_tickets.
resolution string
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-Delivery if 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

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.