Villa Commerce API
Build an alternate shop on Villa Market fulfilment — the same model Ocado uses with Waitrose. You own the customer experience. Villa remains the grocer: catalog identity, branch price, stock, basket, order, payment, and vans.
Public host: https://developers.villamarket.ai
Demo shop and payment links: https://shop.villamarket.ai.
api.villamarket.ai will serve the same API once it moves onto this host's
edge; until then use developers.villamarket.ai in every config.
You never call Villa’s internal website APIs. This host is the only commerce surface partners use.
Surfaces
| Path | Role |
|---|---|
GET /docs |
This guide |
POST /graphql |
GraphQL |
/mcp |
MCP (Streamable HTTP) for agents |
GET /schema.graphql |
Download the SDL |
GET /v1/products… |
Product catalog (REST, cached snapshot) — see below |
GET /pay/<token> |
Payment link: the whole checkout of one order, and where it is paid |
GET /pay/<token>/checkout.json |
The same checkout as JSON (for your app or agent) |
GET /shop |
Demo shop built only on this API — sign up, shop, check out |
GET /health |
Liveness |
Auth
Every GraphQL and MCP call (except public docs) needs:
X-Partner-Key: <key we issue you>
The key maps to your partnerId and registered orderSource (stamped on
quotes and orders).
Shopper-scoped operations (basket, quote, order, payment) also need the customer’s Cognito id token:
Authorization: Bearer <shopper idToken>
ownerId is always the bare Cognito sub from a verified id token.
Do not invent it. Unsigned or forged tokens are rejected.
Shoppers sign up and sign in against the platform's shopper pool (email
and password, a six-digit code by email). Your app talks to Cognito
directly with the pool's public app client id, which we give you with your
key; GET /shop does exactly this in about fifty lines of JavaScript.
Baskets and orders belong to your key and that shopper together — the
same shopper signed in through another partner has a different basket and
sees none of your orders.
Quickstart — GraphQL
curl -sS https://developers.villamarket.ai/graphql \
-H 'Content-Type: application/json' \
-H 'X-Partner-Key: YOUR_KEY' \
-d '{
"query": "query($cpr: ID!, $b: ID!) { product(cprcode: $cpr, branchCode: $b) { cprcode nameEn price { amount currency source } inventory { sellableQty listable } } }",
"variables": { "cpr": "141660", "b": "1000" }
}'
Nested price and inventory are the join win: one client query, branch-
correct numbers. Never display a total that did not come from quote.
Checkout — basket, total, order, payment link
# Where to shop
{ branches { branchCode name } categories { name count } }
query { fulfilmentResolve(input: { lat: 13.7205, lon: 100.5690 }) { branchCode name distanceKm } }
# Browse at a branch: what Villa sells online (onlineOnly, default true), priced at that branch
query { products(filter: { query: "brie", pricedOnly: true }, branchCode: "1030", first: 24) {
totalCount pageInfo { hasNextPage endCursor } nodes { cprcode nameEn imageUrl price { amount } } } }
# Basket (needs the shopper token)
mutation { basketAdd(input: { cprcode: "220772", quantity: 2, branchCode: "1030" }) {
basket { branchCode lines { cprcode quantity product { nameEn } } } userErrors { code message } } }
# The total — Villa's own basket calculator; this number is the bill
mutation { quote(input: { shippingType: "DELIVERY", address: { lat: 13.7205, lon: 100.5690, postcode: "10110" } }) {
quote { grandTotal subTotal deliveryFee totalDiscount lines { cprcode price rowTotal } } userErrors { code message } } }
# Place it — re-priced at this moment; returns the payment link
mutation { orderCreate(input: { shippingType: "PICKUP", specialComment: "test" }) {
order { orderId grandTotal status paymentLink { url expiresAt amount } } userErrors { code message } } }
paymentLink.url opens the whole checkout — items, pickup or delivery,
fees, discounts, total — and takes the payment. Send it to the shopper by
chat, email or a button; …/checkout.json gives your app or agent the same
data. A link is a bearer credential for paying that one order, expires
after 24 hours, and is refused if the order's total no longer matches.
paymentLinkCreate(orderId) issues a fresh one for an unpaid order.
Before a payment is accepted the basket is priced again; a changed total
is refused (PRICE_CHANGED) and the shopper places the order again.
Errors come back in userErrors with a stable code: UNAUTHENTICATED,
EMPTY_BASKET, INVALID_CPRCODE, INVALID_QUANTITY, ADDRESS_REQUIRED
(delivery without coordinates), PRICE_UNAVAILABLE (a product the branch
does not price — remove it or switch branch), NOT_FOUND, ALREADY_PAID.
Quickstart — MCP (Cursor)
Add to .cursor/mcp.json:
{
"mcpServers": {
"villa-partner": {
"url": "https://developers.villamarket.ai/mcp",
"headers": {
"X-Partner-Key": "YOUR_KEY"
}
}
}
}
Tools mirror GraphQL: search_products, get_product, get_inventory,
list_branches, resolve_fulfilment, list_categories, list_products,
get_basket, add_basket_line, set_basket_quantity, empty_basket,
create_quote, apply_coupon, create_order, create_payment_link,
start_payment, get_order, list_orders.
Resources: villa://schema, villa://docs.
Product catalog (REST)
A read-only copy of the full Villa product catalog, refreshed from the master
product data daily and within about 15 minutes of significant changes. It is
served from an in-memory snapshot, so it is fast and safe to poll; use it for
browsing, search suggestions, syncing your own catalog, and feeding agents.
Branch-specific price and stock are not here — use GraphQL product
for those at checkout time.
| Endpoint | Purpose |
|---|---|
GET /v1/products?lang=en&page=1&size=50 |
Paginated list (size ≤ 200). Filters: q= (name, barcode, keywords), category= (any online/villa category level), active=true, fields=cprcode,name,ba_nprice to trim the payload |
GET /v1/products/{cprcode}?lang=en |
One product |
GET /v1/products/manifest |
Snapshot version, build time, row count, file hashes — poll this to know when the catalog changed |
GET /v1/products/download?lang=en |
302 to a five-minute link for the whole catalog as an Apache Feather file (lang=en, th, or all) — the fastest way to sync everything |
langselects the language projection:en(English names and categories) orth(Thai).name,online_category_l1..3,pr_keyword,pr_countrychange with it; identifiers do not.- Price: read
ba_nprice(the base normal price in THB). ThesellingPricefield is carried from the source data but is not maintained there — treat it as absent. Never showba_npriceas the bill; the quote'sgrandTotalis the bill. pr_active,master_online,avail_nationwidetell you whether an item is sellable online;avail_storemaps branch codes to visibility.
Product search
Ranked search over the same catalog, on https://search.api.villamarket.ai.
Hits are identity only (name, barcode, category, image) — never price or
stock. GraphQL search / searchSuggest and MCP search_products use this
engine. Walk the full catalog with /v1/products, not by paging search.
| Endpoint | Purpose |
|---|---|
GET https://search.api.villamarket.ai/v1/search?q=&lang=en&limit=20 |
Hybrid rank (limit ≤ 50). Optional category=, active= |
GET https://search.api.villamarket.ai/v1/suggest?q=&lang=en&limit=10 |
Prefix names and barcode (limit ≤ 30) |
GET https://search.api.villamarket.ai/v1/manifest |
Index version and the catalog snapshot it was built from |
Same X-Partner-Key as the rest of this API. lang is en or th.
- Responses carry
ETagandCache-Control: private, max-age=300, stale-while-revalidate=600— cache them in your own service or app, not in a shared proxy, and sendIf-None-Matchto get a304. Theversionfield in every response identifies the snapshot. X-Partner-Keyis required on every call.
curl -s -H "X-Partner-Key: $KEY" "https://developers.villamarket.ai/v1/products?lang=en&q=cheese&size=5&fields=cprcode,name,ba_nprice,pr_active"
Checkout rules (non-negotiable)
quote.grandTotalis the bill. Never sum line prices in your app.orderCreateprices the order again on our side; you never send a total.orderCreateomits payment. The order carriespaymentLink; payment happens there (paymentStartreturns the same link asredirectUrl). Card PAN never touches this API.- Stock is not reserved at basket-add or at order create.
- Until production go-live every order is a sandbox order:
testis added tospecialComment, nothing is sent for fulfilment, and the payment link's provider issandbox(it records the payment; no money moves).
Discount field contract (coupons / shipping):
https://knowledge.villamarket.ai/reference/discount-fields
Identity keys
| Key | Meaning |
|---|---|
cprcode |
Product identity everywhere |
branchCode |
Fulfilment branch — price authority |
basketId |
Server-side basket, one per (your key, shopper) |
orderId |
Minted by us at orderCreate, e.g. VC260923-3F9A1C07 |
Order state: status is PLACED; payment is payment.isPaid /
payment.status (UNPAID → PAID).
Sandbox vs production
| Sandbox | Production | |
|---|---|---|
| Host | same (developers.villamarket.ai) |
same |
| Keys | sandbox partner key | production partner key |
| Orders | specialComment contains test |
never test in prod |
| Payment | payment link, provider sandbox (records the payment, no money moves) |
payment link, production provider via Villa |
Live money requires: partner key issued, Cognito app client for your redirect URIs, fulfilment branch resolve confirmed, payment + order webhooks registered.
Webhooks (partner → you)
Register a HTTPS endpoint. We POST HMAC-signed events when payment settles or order status moves (once Villa feeds us those events).
POST https://your.example/webhooks/villa-partner
X-Villa-Partner-Signature: sha256=<hex>
Content-Type: application/json
{
"type": "payment.paid | order.status",
"orderId": "…",
"partnerId": "…",
"payload": {},
"ts": 1710000000
}
Until Villa HMAC webhooks exist, we fan out from sanctioned order polls.
Inbound (Villa → us, for our adapter):
POST https://developers.villamarket.ai/webhooks/villa/payment
POST https://developers.villamarket.ai/webhooks/villa/order-status
Go-live checklist
- [ ] Partner key issued;
orderSourceregistered - [ ] Cognito client + redirect URIs for your shop domain
- [ ] Fulfilment resolve tested for your delivery postcodes
- [ ] Quote → order →
paymentStartsandbox closed - [ ] Webhook URL verified (HMAC)
- [ ] Production key rotated; no
testinspecialComment
Support
Engineering vault (public contracts): https://knowledge.villamarket.ai
Ask for a partner key: contact Villa Market AI / partner onboarding.