Skip to content

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.

bash
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

Public API keys carry store scope. Putting one in browser JavaScript leaks access to your conversations and customer data. For client-side flows, use the origin-bound widget key instead - see widget embed codes.

Step-by-step: your first request

  1. In the dashboard, open Settings → API Keys and click Create key (Pro plan or above).
  2. Name the key, pick the read scope, and copy the token from the one-time banner.
  3. Call a read endpoint with the token in the Authorization: Bearer header - e.g. GET /api/public/v1/conversations?limit=5.
  4. Check the JSON response for data and next_cursor; page with cursor until next_cursor is null.
  5. For catalog or order ingest, mint a separate key with the write scope and POST batches 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:

bash
# 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:

json
{
  "error": "Forbidden",
  "message": "Missing scope: write"
}
  • 400 - { "error": "Bad request", "details": { ... } } (or Invalid JSON).
  • 401 - { "error": "Unauthorized" }: missing or invalid key.
  • 403 - { "error": "Forbidden", "message": "Missing scope: ..." }.
  • 422 - every item in an ingest batch failed (see errors).
  • 429 - { "success": false, "error": { "code": "RATE_LIMIT_EXCEEDED", ... } }.

Partial success on ingest

Catalog and order ingest return 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.

json
{
  "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.

POST/api/public/v1/catalog

Upsert 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.

POST/api/public/v1/orders

Upsert 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.

GET/api/public/v1/conversations?limit=50

List conversations with status, intent, sentiment, summary, and tags.

Leads

Read captured leads (read scope). Cursor-paginated.

GET/api/public/v1/leads?limit=50

List 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.

GET/api/public/v1/shop?q=running+shoes&limit=8&in_stock=true

Search 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.

json
{
  "jsonrpc": "2.0", "id": 1, "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18", "capabilities": {},
    "clientInfo": { "name": "merchant-test", "version": "1.0" }
  }
}
json
{
  "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

Missing something, or found an error on this page?

Contact support and include the URL of this page (/docs/api-reference).