● Sandbox openGraphQL · REST · MCPv0.1.0

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.

Two ways in

You are You use You get
A merchant — a marketplace, shop, app or assistant selling to your own customers your merchant key (X-Partner-Key) plus each shopper's sign-in token a royalty on every paid order your shoppers place (below)
An individual — someone shopping for themselves with their own AI a personal key from https://agent.villamarket.ai as Authorization: Bearer vpk_… on the MCP server your own basket and orders on Villa's direct channel; no merchant sees them

Demos of both: https://shop.villamarket.ai (fictional merchants: a storefront, a chat shop, shoppable recipes, their earnings) and https://agent.villamarket.ai (Villa's own agent and the connect guide for your AI).


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, /shop/lumora, /shop/suk, /shop/krua, /shop/earnings Fictional merchant demos built only on this API, and their earnings
POST /agent/chat, GET /agent/status Hosted shopping assistant (see below)
GET /agent, /llms.txt, /.well-known/mcp.json Villa's own agent page, and how an AI agent connects
GET /health Liveness

Auth

Send your key on every GraphQL and MCP call:

X-Partner-Key: <key we issue you>

Today the catalog also answers without a key: products, categories and branches (the anonymous-catalog policy is still open). Anything with a basket, order, payment or earnings needs the key.

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.

# 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 Market's own online shop shows at that branch (onlineOnly, default true),
# with that branch's prices. A product Villa's own shop hides at a branch is not listed there.
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_SOLD_AT_BRANCH (Villa's shop does not sell that product at the basket's branch), NOT_FOUND, ALREADY_PAID.


Quickstart — MCP

The server is https://agent.villamarket.ai/mcp/ (also on this host at /mcp/), Streamable HTTP. Identity comes from the connection's headers. The shopper tools still list an optional id_token argument for older clients. Leave it empty and send the headers; it will be removed:

Cursor (.cursor/mcp.json):

{
  "mcpServers": {
    "villa": {
      "url": "https://agent.villamarket.ai/mcp/",
      "headers": { "Authorization": "Bearer vpk_YOUR_PERSONAL_KEY" }
    }
  }
}

Claude Code: claude mcp add --transport http villa https://agent.villamarket.ai/mcp/ --header "Authorization: Bearer vpk_…". ChatGPT and Claude apps connect with OAuth; Villa's OAuth sign-in is not live yet. Discovery for agents: https://agent.villamarket.ai/llms.txt and https://agent.villamarket.ai/.well-known/mcp.json. Every tool carries a title and a read-only or destructive hint; create_order and start_payment are destructive, so clients ask the shopper before running them.

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.


Merchant royalty

Each merchant key carries a royalty rate. Every order placed with your key records it; it accrues when the shopper pays:

royalty = rate × (grandTotal − delivery fee after discounts − express shipping)
PENDING (placed, unpaid) → ACCRUED (paid)
{ merchantEarnings { royaltyRate orders paidOrders paidSales royaltyAccrued royaltyPending
    recent { orderId createdAt grandTotal paid royalty { rate base amount status } } } }

merchantEarnings needs only your merchant key and returns no shopper identity, names or addresses. Every Order also carries royalty { rate base amount status }. Settlement of accrued royalty is monthly and outside the sandbox.

Hosted agent (optional)

POST /agent/chat runs one turn of Villa's shopping assistant for your shopper, in your brand: X-Partner-Key + the shopper's Authorization: Bearer <idToken>, body {"message", "history": [{"role": "user"|"assistant", "content"}], "branchCode", "location": {"lat", "lon"}}. It answers {reply, cards, activity, usage}; cards are products, basket, total, an order to confirm, a payment link. The assistant never places an order — your page shows the confirm card and calls orderCreate on the shopper's click. Limits per shopper and per merchant per day come back as HTTP 429 with a code; GET /agent/status says whether it is switched on. Prompts are never shown your shopper's contact details or location.


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

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.

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)

  1. quote.grandTotal is the bill. Never sum line prices in your app. orderCreate prices the order again on our side; you never send a total.
  2. orderCreate omits payment. The order carries paymentLink; payment happens there (paymentStart returns the same link as redirectUrl). Card PAN never touches this API.
  3. Stock is not reserved at basket-add or at order create.
  4. Until production go-live every order is a sandbox order: test is added to specialComment, nothing is sent for fulfilment, and the payment link's provider is sandbox (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)

Not available yet. POST /webhooks/register answers 501. Until webhooks are durable (a stored registration, HMAC-signed deliveries, retries), poll order(orderId) or orders for payment and status changes.

When they arrive, events will look like this, signed with X-Villa-Partner-Signature: sha256=<hex>:

{
  "type": "payment.paid | order.status",
  "orderId": "…",
  "partnerId": "…",
  "payload": {},
  "ts": 1710000000
}

Go-live checklist


Support

Engineering vault (public contracts): https://knowledge.villamarket.ai
Ask for a partner key: contact Villa Market AI / partner onboarding.