REST API reference for custom and headless stores
Zubby exposes a public REST API at https://app.zubbyai.com/api/public/v1/. Use it to push your catalog and orders (the primary path for custom / headless stores) and to read conversations and leads back out into your CDP or warehouse. The API is JSON in, JSON out, cursor-paginated, and idempotent on writes.
Authentication
Every catalog, order, conversation, lead, and product-search request carries a Bearer API key. The key is bound to one store, so there is no separate store-ID header - the store is derived from the key. The OpenAPI document and keyless MCP endpoint are the documented exceptions. Mint keys from Dashboard → Settings → API Keys (public API keys are available on the Pro plan and above). The secret is returned once at creation - store it in a secret manager, never commit it.
curl https://app.zubbyai.com/api/public/v1/conversations \
-H "Authorization: Bearer sp_live_<prefix>.<secret>"Merchant-created keys carry scopes: read (supported data reads, including POST /loyalty/status) and write (catalog/order ingestion and supported write operations such as loyalty redemption). Endpoint-specific entitlement and identity checks still apply. A request to a write endpoint with a read-only key returns 403.
Don’t expose keys client-side
Step-by-step: your first request
- In the dashboard, open
Settings → API Keysand click Create key (Pro plan or above). - Name the key, pick the
readscope, and copy the token from the one-time banner. - Call a read endpoint with the token in the
Authorization: Bearerheader - e.g.GET /api/public/v1/conversations?limit=5. - Check the JSON response for
dataandnext_cursor; page withcursoruntilnext_cursorisnull. - For catalog or order ingest, mint a separate key with the
writescope andPOSTbatches of up to 200 items.
Rate limits
60 requests / minute per API key (a sliding window, bucketed on the key). When you exceed it the gateway returns 429 with these headers:
Retry-After- seconds to wait before retrying.X-RateLimit-Limit- your ceiling (60).X-RateLimit-Remaining- calls left in the current window.X-RateLimit-Reset- unix epoch (ms) when the window resets.
For the high-volume initial catalog load, batch up to 200 products per POST /catalog rather than sending one request per product.
Pagination
The read endpoints (/conversations, /leads) use cursor pagination. Pass limit (max 100) and the next_cursor from the previous response as cursor:
# First page
curl "https://app.zubbyai.com/api/public/v1/conversations?limit=50" \
-H "Authorization: Bearer sp_live_<prefix>.<secret>"
# Response:
# {
# "data": [ ... ],
# "next_cursor": "2026-04-28T12:00:00.000Z"
# }
# Next page
curl "https://app.zubbyai.com/api/public/v1/conversations?limit=50&cursor=2026-04-28T12:00:00.000Z" \
-H "Authorization: Bearer sp_live_<prefix>.<secret>"The cursor is the ISO timestamp of the last row. When next_cursor is null, you’ve reached the end.
Errors
The endpoints return a non-2xx status with a JSON body. Validation failures include a details object from the schema:
{
"error": "Forbidden",
"message": "Missing scope: write"
}400-{ "error": "Bad request", "details": { ... } }(orInvalid JSON).401-{ "error": "Unauthorized" }: missing or invalid key.403-{ "error": "Forbidden", "message": "Missing scope: ..." }.422- every item in an ingest batch failed (seeerrors).429-{ "success": false, "error": { "code": "RATE_LIMIT_EXCEEDED", ... } }.
Partial success on ingest
200 with { "upserted", "received", "errors": [...] }. A non-empty errors array on a 200 means some items succeeded and some failed - always inspect it.Public read scope
The public conversation and lead endpoints support scoped reads. Catalog and order endpoints accept ingest writes. There are no general public customer, revenue-report, or historical order-list read endpoints. Use the merchant Reports page for its documented CSV exports; a merchant session endpoint is not interchangeable with a public Bearer API.
Headless loyalty
Your authenticated backend can use POST /api/public/v1/loyalty/statuswith read scope, or POST /api/public/v1/loyalty/redeem with write scope. Both also require loyalty access. The backend must vouch for the signed-in customer: never pass an arbitrary shopper-entered email through as verified identity, or expose the API key in a browser. The status body below is synthetic. Redemption adds a reward UUID and an 8–100-character idempotencyKey; reuse that key for the same attempt so a retry does not spend points twice. Read the returnedok and error fields even on HTTP 200.
{
"customer": { "id": "customer-demo-42", "email": "shopper@example.com" }
}Loyalty setup and reward limits explains earn rules, eligibility, and platform-specific fulfillment.
Endpoint reference
Each endpoint group below lists the route, the HTTP method, a one- line summary, and an example body when relevant.
Catalog ingest
Push products + variants (write scope). Mainly for custom / headless stores; Shopify and Woo sync automatically. Idempotent on your externalId. Max 200 products per request.
/api/public/v1/catalogUpsert products + variants and queue embeddings. Re-send the same externalId to update in place.
{
"products": [
{
"externalId": "SKU-1",
"title": "Waxed Field Jacket",
"description": "Waxed cotton, charcoal.",
"status": "active",
"url": "https://shop.yourbrand.com/p/field-jacket",
"imageUrl": "https://cdn.yourbrand.com/jacket.jpg",
"tags": ["jackets"],
"variants": [
{ "externalId": "SKU-1-M", "title": "Medium", "price": 189.00, "inventory": 12 }
]
}
]
}Orders ingest
Push orders + line items (write scope) so order-status, reorder, AI attribution, profit, post-purchase journeys and loyalty work on custom stores. Idempotent on externalId; line items are replaced on re-push. Send orderedAt, and optionally the widget's shopperSessionId for exact attribution.
/api/public/v1/ordersUpsert orders and their items. Re-push to update financial / fulfillment status.
{
"orders": [
{
"externalId": "1001",
"email": "shopper@example.com",
"orderedAt": "2026-10-21T10:00:00Z",
"currency": "USD",
"total": 199.00,
"financialStatus": "paid",
"fulfillmentStatus": "unfulfilled",
"items": [
{ "title": "Waxed Field Jacket", "platformVariantId": "SKU-1-M", "quantity": 1, "unitPrice": 189.00 }
]
}
]
}Conversations
Read shopper ↔ AI sessions (read scope). Cursor-paginated. Mirror into your CDP or warehouse.
/api/public/v1/conversations?limit=50List conversations with status, intent, sentiment, summary, and tags.
Leads
Read captured leads (read scope). Cursor-paginated.
/api/public/v1/leads?limit=50List leads with email, name, purpose, status, and priority.
Agentic commerce
Let external AI agents (ChatGPT, Claude, custom assistants) search your catalog and get buy-ready results (read scope). Returns products with price, availability, and supported purchase links. Custom stores use their supplied product URL; a platform-specific cart link is not guaranteed.
/api/public/v1/shop?q=running+shoes&limit=8&in_stock=trueSearch the catalog with the same hybrid AI retrieval the widget uses. limit max 50 (default 12).
Idempotency
Ingest writes are idempotent on your own identifiers - no Idempotency-Key header needed. A product is keyed on its externalId, an order on its externalId. Re-sending the same payload updates the existing record in place instead of creating a duplicate. Inspect per-item errors before retrying failed records; a 200 response can still contain failures. Preserve the original order timestamp during status updates. Omitting orderedAt treats an order as historical and suppresses live-order side effects.
OpenAPI spec & MCP
A machine-readable OpenAPI 3.1 spec is served at GET /api/public/v1/openapi.json (no auth required to read it) - import it into Postman, Insomnia, or an SDK generator. There is also a keyless Model Context Protocol endpoint at POST /api/public/v1/mcp/{storeId} that exposes read-only catalog and published-policy tools (search_products, get_product, get_store_info, search_policies) to external AI agents. It is rate-limited per IP. The store must be active, not billing-suspended, and on a plan with public API access. This endpoint exposes public read-only data; it cannot place an order or mutate a cart.
Send these synthetic JSON-RPC bodies as separate HTTP POST requests with Content-Type: application/json to your store endpoint. The server is stateless: there is no session or server-sent event stream. Inspect both JSON-RPC errors and tool isError, since an HTTP 200 can contain a failed tool call.
{
"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {
"protocolVersion": "2025-06-18", "capabilities": {},
"clientInfo": { "name": "merchant-test", "version": "1.0" }
}
}{
"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {
"name": "search_products",
"arguments": { "query": "waxed cotton jacket", "limit": 3 }
}
}Verify the returned product URLs, prices, and availability against the source store. Custom-store freshness depends on the latest successful ingest. A valid response does not mean an external assistant will select the product, list it, or cite your store.
SDKs
There’s no official SDK yet - the API is plain HTTP + JSON and works from any language with an HTTP client. If you’d like a typed client for a specific language, let us know and we’ll prioritize.
Adjacent
- Custom / headless integration - the end-to-end guide to connecting a store with this API.
- API keys - minting read vs. write keys and revoking them.
- Outbound webhooks - push, not pull.
- Security & privacy - how we handle your API keys.