{"openapi":"3.1.0","info":{"title":"Zubby AI Public API","version":"1.0.0","description":"Read store leads/conversations, ingest catalog/orders for headless or custom storefronts, and run agentic product search. Authenticate with a store API key minted in Dashboard → Settings → API Keys. Ingest writes are idempotent on your own identifiers (`externalId`) - re-sending the same id upserts rather than duplicates."},"servers":[{"url":"/api/public/v1","description":"Public API base path (relative to the app origin)."}],"security":[{"bearerAuth":[]}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer","description":"Authorization: Bearer sp_live_<prefix>.<secret>. The secret is shown once at mint time. Keys carry `read` and/or `write` scopes; missing scope returns 403."}},"responses":{"Unauthorized":{"description":"Missing or invalid API key.","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"}}}}}},"Forbidden":{"description":"Key lacks the required scope (read/write).","content":{"application/json":{"schema":{"type":"object","properties":{"error":{"type":"string"},"message":{"type":"string"}}}}}},"RateLimited":{"description":"Rate limit exceeded (per key prefix + IP)."}},"schemas":{"Lead":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"email":{"type":["string","null"]},"name":{"type":["string","null"]},"purpose":{"type":"string"},"status":{"type":"string"},"priority":{"type":"string"},"created_at":{"type":"string","format":"date-time"}}},"Conversation":{"type":"object","properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string"},"started_at":{"type":"string","format":"date-time"},"last_message_at":{"type":"string","format":"date-time"},"intent":{"type":["string","null"]},"sentiment_label":{"type":["string","null"]},"summary":{"type":["string","null"]},"auto_tags":{"type":"array","items":{"type":"string"}},"manual_tags":{"type":"array","items":{"type":"string"}},"escalated_at":{"type":["string","null"],"format":"date-time"}}},"Money":{"description":"Monetary amount - accepts a number (12.5) or a decimal string (\"12.50\").","type":["number","string"]},"NonNegativeMoney":{"description":"Non-negative amount in the store currency - a number (12.5) or a plain decimal string (\"12.50\"), at most 9999999999.99. Rounded to cents. Negative values, currency symbols and thousands separators are rejected with 400.","type":["number","string"]},"ProductInput":{"type":"object","required":["externalId","title"],"properties":{"externalId":{"type":"string","maxLength":255,"description":"Your stable product id; the upsert key."},"title":{"type":"string","maxLength":500},"description":{"type":"string","maxLength":20000},"vendor":{"type":"string","maxLength":255},"productType":{"type":"string","maxLength":255},"status":{"type":"string","enum":["active","draft","archived"]},"url":{"type":"string","format":"uri","maxLength":1000},"imageUrl":{"type":"string","format":"uri","maxLength":1000},"tags":{"type":"array","maxItems":50,"items":{"type":"string","maxLength":120}},"variants":{"type":"array","maxItems":100,"items":{"type":"object","required":["externalId","title"],"properties":{"externalId":{"type":"string","maxLength":255},"title":{"type":"string","maxLength":500},"sku":{"type":"string","maxLength":255},"price":{"$ref":"#/components/schemas/Money"},"compareAtPrice":{"$ref":"#/components/schemas/Money"},"inventory":{"type":"integer","minimum":0},"inventoryTracked":{"type":"boolean","description":"true when you track stock for this variant AND stop selling at zero, so an inventory of 0 means sold out. false for untracked items or ones that keep selling (backorders). Omit to keep the stored value; until it is set, the variant is never treated as sold out."},"requiresShipping":{"type":"boolean"},"imageUrl":{"type":"string","format":"uri","maxLength":1000},"cost":{"oneOf":[{"$ref":"#/components/schemas/NonNegativeMoney"},{"type":"null"}],"description":"Per-unit cost of goods. Powers profit analytics and margin-aware recommendations. Omit to keep the stored cost; null clears it."}}}}}},"OrderInput":{"type":"object","required":["externalId"],"properties":{"externalId":{"type":"string","maxLength":255,"description":"Your stable order id; the upsert key."},"email":{"type":"string","format":"email","maxLength":320},"currency":{"type":"string","maxLength":10,"default":"USD"},"subtotal":{"$ref":"#/components/schemas/Money"},"total":{"$ref":"#/components/schemas/Money"},"financialStatus":{"type":"string","maxLength":32},"fulfillmentStatus":{"type":"string","maxLength":32},"status":{"type":"string","maxLength":32},"shippingCountry":{"type":"string","maxLength":8},"shippingZip":{"type":"string","maxLength":32},"orderedAt":{"type":"string","format":"date-time","description":"When the order was placed. Set on first insert only; re-pushing an order preserves the original timestamp. Orders placed more than 48 hours ago are recorded in full (attribution, profit, shopper totals, loyalty) but send no journeys, AI-sale notification or ad-platform events, so history imports are safe. An order first sent WITHOUT orderedAt is treated the same way, because its placement time is unknown: send it for live orders."},"cartToken":{"type":"string","maxLength":255,"description":"The storefront cart id this order checked out, as handed to the widget with the zubby:cart event. Matches the order to the cart and to products the AI recommended while that cart was open."},"shopperSessionId":{"type":"string","format":"uuid","description":"The widget session id, read in the browser at checkout from localStorage key salespilot:shopper-session:<storeId>. The most exact AI attribution evidence: recommendations made in that chat session are credited to this order."},"discountAmount":{"$ref":"#/components/schemas/NonNegativeMoney","description":"Order-level discount. Omit on a re-push to keep the stored value."},"paymentFee":{"$ref":"#/components/schemas/NonNegativeMoney","description":"The payment processor's actual fee for this order. When omitted, the fee is estimated from the store's payment fee rates and flagged as an estimate; an actual fee is never replaced by an estimate."},"paymentGateway":{"type":"string","maxLength":64,"description":"Gateway id (e.g. \"stripe\", \"paypal\", \"cod\") used to pick the fee rate when paymentFee is omitted."},"shippingCost":{"$ref":"#/components/schemas/NonNegativeMoney","description":"What you paid to ship this order (not what the shopper was charged). Omit on a re-push to keep the stored value."},"items":{"type":"array","maxItems":200,"items":{"type":"object","required":["title"],"properties":{"title":{"type":"string","maxLength":500},"platformProductId":{"type":"string","maxLength":255},"platformVariantId":{"type":"string","maxLength":255},"quantity":{"type":"integer","minimum":1,"default":1},"unitPrice":{"$ref":"#/components/schemas/Money"},"lineTotal":{"$ref":"#/components/schemas/Money"},"cost":{"$ref":"#/components/schemas/NonNegativeMoney","description":"Per-unit cost of goods at the time of sale; replaces the line's stored cost. When omitted, the matched catalog variant's cost is snapshotted the first time the line is recorded and kept on later re-pushes."}}}}}},"ShopperIdentity":{"type":"object","description":"Not an API request body: the signed-in customer your storefront hands to the widget (window.zubbyIdentity or the zubby:identify event), which unlocks order lookups and loyalty for that customer in the widget. Mint it on your SERVER with the signing secret from Dashboard > Settings > API Keys > Storefront identity (custom stores): idToken = \"v1.\" + exp + \".\" + hex(HMAC_SHA256(secret, storeId + \"|\" + externalId + \"|\" + lowercase(trim(email)) + \"|\" + exp)), where exp is a unix-seconds expiry at most 24 hours ahead (30 minutes recommended) and the secret string is used as-is as the HMAC key. Rotating or revoking the secret invalidates every token signed with it. See /docs/integrations/custom#identity.","required":["idToken"],"properties":{"email":{"type":"string","format":"email","description":"The customer's email, exactly as signed (case-insensitive). Required for order lookups."},"externalId":{"type":"string","maxLength":255,"description":"Your customer id, exactly as signed (no surrounding whitespace). Use an empty string in the HMAC when omitted."},"name":{"type":"string","maxLength":200,"description":"Display name for the greeting; not signed."},"idToken":{"type":"string","maxLength":512,"pattern":"^v1\\.[0-9]+\\.[0-9a-f]{64}$"}}},"ShopProduct":{"type":"object","properties":{"id":{"type":"string","description":"Platform product id."},"title":{"type":"string"},"vendor":{"type":["string","null"]},"type":{"type":["string","null"],"description":"Product type/category."},"price":{"type":"string","description":"Decimal string, e.g. \"49.00\"."},"compareAtPrice":{"type":["string","null"],"description":"Decimal string or null."},"currency":{"type":"string"},"available":{"type":"boolean"},"url":{"type":["string","null"],"format":"uri"},"addToCartUrl":{"type":["string","null"],"description":"Buy-ready deep link."},"imageUrl":{"type":["string","null"]}}}}},"paths":{"/leads":{"get":{"summary":"List leads","description":"Paginated, newest-first list of captured shopper leads for the store.","security":[{"bearerAuth":[]}],"x-required-scope":"read","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string","format":"date-time"},"description":"ISO timestamp from a prior next_cursor."}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Lead"}},"next_cursor":{"type":["string","null"],"description":"ISO-8601 timestamp; pass as ?cursor= to fetch the next page. null when exhausted."}},"required":["data","next_cursor"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/conversations":{"get":{"summary":"List conversations","description":"Paginated list of widget conversations (status, intent, sentiment_label, auto/manual tags), newest last_message_at first. Cursor is the last row's last_message_at.","security":[{"bearerAuth":[]}],"x-required-scope":"read","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":50}},{"name":"cursor","in":"query","schema":{"type":"string","format":"date-time"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"data":{"type":"array","items":{"$ref":"#/components/schemas/Conversation"}},"next_cursor":{"type":["string","null"],"description":"ISO-8601 timestamp; pass as ?cursor= to fetch the next page. null when exhausted."}},"required":["data","next_cursor"]}}}},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"429":{"$ref":"#/components/responses/RateLimited"}}}},"/catalog":{"post":{"summary":"Ingest products","description":"Upsert up to 200 products (each with up to 100 variants), keyed on externalId. Enqueues embeddings so ingested products become AI-searchable.","security":[{"bearerAuth":[]}],"x-required-scope":"write","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["products"],"properties":{"products":{"type":"array","maxItems":200,"items":{"$ref":"#/components/schemas/ProductInput"}}}}}}},"responses":{"200":{"description":"Processed (some rows may have errored - see errors[]).","content":{"application/json":{"schema":{"type":"object","properties":{"upserted":{"type":"integer","description":"Rows inserted or updated."},"received":{"type":"integer","description":"Rows in the request body."},"errors":{"type":"array","items":{"type":"object","properties":{"externalId":{"type":"string"},"error":{"type":"string"}}}}},"required":["upserted","received","errors"]}}}},"400":{"description":"Invalid JSON or body."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"All rows failed.","content":{"application/json":{"schema":{"type":"object","properties":{"upserted":{"type":"integer","description":"Rows inserted or updated."},"received":{"type":"integer","description":"Rows in the request body."},"errors":{"type":"array","items":{"type":"object","properties":{"externalId":{"type":"string"},"error":{"type":"string"}}}}},"required":["upserted","received","errors"]}}}}}}},"/orders":{"post":{"summary":"Ingest orders","description":"Upsert up to 200 orders keyed on externalId; line items are replaced on re-push, each keeping its cost snapshot. Orders go through the same pipeline as Shopify and WooCommerce orders: AI attribution (revenue and profit), post-purchase journeys, AI-sale notifications, shopper totals and loyalty points, each first-time effect exactly once per order. Powers order status and profit for headless stores. Per-order errors carry the code ORDER_INGEST_FAILED; retrying the same order is safe.","security":[{"bearerAuth":[]}],"x-required-scope":"write","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["orders"],"properties":{"orders":{"type":"array","minItems":1,"maxItems":200,"items":{"$ref":"#/components/schemas/OrderInput"}}}}}}},"responses":{"200":{"description":"Processed (some rows may have errored - see errors[]).","content":{"application/json":{"schema":{"type":"object","properties":{"upserted":{"type":"integer","description":"Rows inserted or updated."},"received":{"type":"integer","description":"Rows in the request body."},"errors":{"type":"array","items":{"type":"object","properties":{"externalId":{"type":"string"},"error":{"type":"string"}}}}},"required":["upserted","received","errors"]}}}},"400":{"description":"Invalid JSON or body."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"422":{"description":"All rows failed.","content":{"application/json":{"schema":{"type":"object","properties":{"upserted":{"type":"integer","description":"Rows inserted or updated."},"received":{"type":"integer","description":"Rows in the request body."},"errors":{"type":"array","items":{"type":"object","properties":{"externalId":{"type":"string"},"error":{"type":"string"}}}}},"required":["upserted","received","errors"]}}}}}}},"/loyalty/status":{"post":{"summary":"Loyalty status for a customer","description":"Points balance, tier and the reward catalog (affordable, in stock, delivery type) for a customer your backend identifies. Requires a plan with loyalty.","security":[{"bearerAuth":[]}],"x-required-scope":"read","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customer"],"properties":{"customer":{"type":"object","description":"Identify the customer by id and/or email (at least one).","properties":{"id":{"type":"string","description":"Your platform customer id."},"email":{"type":"string","format":"email"}}}}}}}},"responses":{"200":{"description":"Status; found=false when no shopper matches (balance 0)."},"400":{"description":"Invalid body."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/loyalty/redeem":{"post":{"summary":"Redeem a loyalty reward","description":"Spends points on a reward and returns the discount code (code pool, shared code or WooCommerce auto-generated coupon) or a manual-fulfilment notice. Idempotent per idempotencyKey: retries return the same result and never spend twice.","security":[{"bearerAuth":[]}],"x-required-scope":"write","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["customer","rewardId","idempotencyKey"],"properties":{"customer":{"type":"object","properties":{"id":{"type":"string","description":"Your platform customer id."},"email":{"type":"string","format":"email"}}},"rewardId":{"type":"string","format":"uuid"},"idempotencyKey":{"type":"string","pattern":"^[A-Za-z0-9:_.-]{8,100}$"}}}}}},"responses":{"200":{"description":"Redemption result: ok=true with code / fulfilment, or ok=false with a reason such as insufficient_points or out_of_stock."},"400":{"description":"Invalid body."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"}}}},"/shop":{"get":{"summary":"Agentic product search","description":"Hybrid AI product search returning buy-ready products with add-to-cart deep links. CORS-enabled for external AI agents; cached ~60s.","security":[{"bearerAuth":[]}],"x-required-scope":"read","parameters":[{"name":"q","in":"query","schema":{"type":"string"},"description":"Search query. Empty returns popular products."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":12}},{"name":"in_stock","in":"query","schema":{"type":"boolean"}}],"responses":{"200":{"description":"OK","content":{"application/json":{"schema":{"type":"object","properties":{"store":{"type":"object","properties":{"name":{"type":"string"},"domain":{"type":"string"},"platform":{"type":["string","null"]},"currency":{"type":"string"}}},"query":{"type":["string","null"]},"count":{"type":"integer"},"products":{"type":"array","items":{"$ref":"#/components/schemas/ShopProduct"}}}}}}},"400":{"description":"Invalid query parameters."},"401":{"$ref":"#/components/responses/Unauthorized"},"403":{"$ref":"#/components/responses/Forbidden"},"404":{"description":"Store not found."}}}}}}