# unhuman shopping Hello, agent. You found the everything store. Buy products from Amazon, Walmart, Nordstrom, Gap, Old Navy, Banana Republic, and any Shopify-powered store via API, paid with Bitcoin Lightning (L402). Search first, quote each item, then place one same-retailer purchase (exception: gap, oldnavy, and bananarepublic share one Gap Inc. cart and may mix in a single purchase). Nordstrom and Gap-family discovery is powered by Channel3, exact quotes are verified through Channel3 lookup, and orders are placed through Zinc with the selected product URL. retailer=shopify searches every Shopify merchant at once; each result's retailer is that merchant's own storefront domain, and its quote is an estimated total (the listing price plus a buffer standing in for shipping/tax, requires zip) fulfilled through Zinc — any unused buffer is refunded to your refund_destination after the order. Already have a Shopify product-page URL (…/products/{handle})? Pass it as q with retailer={store domain} (or retailer=shopify) and the catalog performs an exact lookup of that product instead of a keyword search — use this whenever a store's item does not surface in search results. > **US only.** Shipping is limited to addresses within the United States. ## Recommended Workflow 1. Search for products → GET /api/catalog?q={query}&retailer={retailer}&zip={zip} (free) 2. Pick one or more products from the same retailer — note each product_id, retailer, and quote_url 3. Need more product photos without a quote → GET /api/catalog/images?url={product_url} (free, optional) 4. Get an exact quote for each product line → GET /api/catalog/{productId}?retailer={retailer}&zip={zip} (free). Shopify-merchant and cross-retailer results (target, bestbuy, ...) are quoted by following the search result's quote_url instead — GET /api/catalog/quote (free; see Quote by URL). A Shopify line can optionally be upgraded to an address-exact total → POST /api/catalog/quote (free; see Exact Shopify Quote) 5. Preview the whole basket's real total → POST /api/order/preview with all the quote_tokens (free, read-only; returns the single order-level service fee plus buffer-inclusive totals). Show the response's charge_amount_cents as the price before charging — do not sum per-line fees yourself. 6. Place the order → POST /api/order with each distinct line's quote_token, a quantity (integer 1-100; use one line per distinct product), and shipping address. Split the purchase by retailer when retailers differ. ## Endpoints Base URL: https://unhuman.shopping ### Search Products (FREE) GET /api/catalog?q={query}&retailer={retailer}&zip={zip}&page={page} Search supported retailers for products. Free — no payment required. This endpoint is optimized for cheap discovery, not final checkout pricing, even when q already names a specific product ID. Query parameters: - q (required): Search query or retailer product ID (e.g. "wireless earbuds", "B0CX23V2ZK", "2101922242", "n4RTalM") - retailer (optional): Scope to search. Default: "amazon". Supported values: - "all": Federated search across the cross-retailer allowlist stores (Amazon, Walmart, Target, Best Buy, ...), every Shopify merchant, and Nordstrom in parallel. NOT included: gap, oldnavy, bananarepublic — search those retailers directly. Each result carries its real retailer. Slow/failing sources degrade to partial results (see sources in the response) - "amazon", "walmart", "nordstrom", "gap", "oldnavy", "bananarepublic": One first-class retailer - "shopify": Every Shopify merchant's storefront at once; each result carries the merchant's own domain as its retailer (e.g. "flightcoffeeco.com") - A merchant domain (any value containing a dot, e.g. "flightcoffeeco.com"): Just that Shopify merchant's products - An allowlist merchant name ("target", "bestbuy", "homedepot", "lowes", "costco", "wayfair", "macys", "kohls", "staples"): That store via cross-retailer search - zip (optional): Delivery ZIP code used when building quote_url - page (optional): Result page (default 1) - page_token (optional): Opaque pagination token for Channel3-retailer (nordstrom/gap/oldnavy/bananarepublic) and Shopify searches - sources (optional, retailer=all only): Comma-separated subset of the federated scopes to fan out to — "supported_retailers", "shopify_merchants", "nordstrom". Default: all three Filter parameters (optional; Shopify's vocabulary): filters are passed through ONLY to sources that support them natively and are dropped for the rest — a dropped filter means that source's results are unconstrained on that dimension (reported in sources[].dropped_filters on retailer=all). Shopify honors the supported vocabulary; the Channel3 retailers (nordstrom/gap/oldnavy/bananarepublic) honor price and available=true; Amazon/Walmart/cross-retailer honor none. - price_min / price_max: Price bounds in integer cents - available: "true" for quoteable in-stock results. available=false is not supported and returns 400 - ships_to: ISO 3166-1 country code (e.g. "US") Response: JSON object containing: - products: Array of products, each with: - asin: Amazon ASIN when retailer is "amazon" - channel3_product_id: Channel3 product ID for Channel3-sourced retailers (nordstrom/gap/oldnavy/bananarepublic) - product_id: Retailer product ID - retailer: Retailer name - title: Product name - retailer_price_cents: Retailer product price in cents, or null when unverified - amazon_price_cents: Amazon product price in cents (included for backward compatibility), or null when unverified - unverified_retailer_price_cents: Channel3's unverified price in cents for Channel3-sourced retailers - price_status: "verified" or "unverified" - shipping_estimate_cents: Shipping estimate when already known, otherwise null - total_cents: Product + shipping when shipping is already known, otherwise null - service_fee_cents: Service fee when total_cents is known, otherwise null - estimated_checkout_total_cents: All-in checkout estimate (goods + service fee + order-processing fees) when enough pricing data is available, otherwise null. This is the ONE price to show the customer — identical for Lightning and card for buffered quotes (an exact Shopify quote later supersedes it and charges Lightning less; see Exact Shopify Quote); when null (shipping/tax unknown until the quote), show the product price instead — retailer_price_cents when present, otherwise unverified_retailer_price_cents — and get the all-in number from the quote - currency: "USD" - product_url: Retailer product URL - url: Retailer product URL - thumbnail: Product image URL - images: Array of product image URLs (primary image first; may be empty). Search results usually carry one image — the full gallery comes from the quote response - rating: Star rating when available - is_prime: Always null in search results — Prime eligibility is unknown until the quote response resolves it - quote_required: Always true for search results - quote_available: Whether quote_url can be called for exact checkout pricing - quote_pricing_source: "channel3_lookup" for Channel3-retailer results; "shopify_checkout" for Shopify-merchant results - brand: Store/brand name when known (Shopify results carry the merchant's store name) - details: Short merchant-authored product detail (materials, specs) when available - quote_url: Call this next to get one quote_token for that chosen product line. Follow it verbatim — for Shopify-merchant and cross-retailer results it points at GET /api/catalog/quote, and Shopify quote_urls are signed capabilities that cannot be reconstructed by hand. For Shopify-merchant results the quote is an estimated total: the listing price plus a buffer standing in for shipping/tax (requires zip), fulfilled through Zinc; any unused buffer is refunded to your refund_destination after the order - query: The search query - page: Current page number - next_page_token: Opaque Channel3 pagination token for the next Channel3-retailer page, when available - retailer: The scope searched - sources (retailer=all only): Per-store-scope diagnostics — { source, status: "ok"|"timeout"|"error", count, dropped_filters }. source is one of "supported_retailers", "shopify_merchants", or "nordstrom". A non-ok source means its stores are missing from this result set, not that no products exist there Example: GET /api/catalog?q=wireless+earbuds&retailer=walmart&zip=30312 GET /api/catalog?q=dress&retailer=nordstrom&zip=30312 ### Product Images (FREE) GET /api/catalog/images?url={productUrl} Get the full image gallery and basic metadata for a retailer product URL (e.g. the product_url from a search result) without minting a quote. Amazon and Walmart galleries come from retailer product data; other domains come from a third-party product index. Prices are unverified — use quote_url for checkout pricing. The first request for a product may take a few seconds; repeat requests are fast. Most useful for amazon and walmart search results, which usually carry one image. Channel3-retailer search results already include their gallery in the images field. Query parameters: - url (required): Percent-encoded retailer product URL Response: JSON object containing: - product: - title: Product name - url: The requested retailer product URL - thumbnail: Primary product image URL, when available - images: Array of product image URLs (primary image first; may be empty) - unverified_price_cents: Unverified product price in cents, when available - currency: Price currency, when a price is present - retailer_domain: Registrable retailer domain of the requested URL (e.g. "amazon.com") Returns 404 when the product index resolves a non-amazon/walmart URL to a product sold on a different retailer's domain, and 400 when url is missing or not a valid URL. Example: GET /api/catalog/images?url=https%3A%2F%2Fwww.amazon.com%2Fdp%2FB0CX23V2ZK ### Quote One Product (FREE) GET /api/catalog/{productId}?retailer={retailer}&zip={zip} Get exact checkout pricing and a quote_token for one chosen Amazon, Walmart, Nordstrom, Gap, Old Navy, or Banana Republic product line. Channel3-retailer quote pricing is verified through Channel3 lookup before the quote_token is minted. Gap-family quotes include an estimated guest-shipping line (~$9.50 standard; guests have no free-shipping threshold); any overestimate is refunded after the order. Query parameters: - retailer (optional): Retailer to quote. Supported values: "amazon", "walmart", "nordstrom", "gap", "oldnavy", "bananarepublic". Default: "amazon" - zip (optional): Delivery ZIP code to associate with the quote Response: JSON object containing: - product: - asin: Amazon product ID when retailer is "amazon" - channel3_product_id: Channel3 product ID for Channel3-sourced retailers (nordstrom/gap/oldnavy/bananarepublic) - product_id: Retailer product ID - retailer: Retailer name - title: Product name - retailer_price_cents: Retailer product price in cents - amazon_price_cents: Amazon product price in cents (included for backward compatibility) - shipping_estimate_cents: Real shipping estimate in cents (actual carrier/retailer shipping only; the tax + not-yet-final-shipping headroom is the separate estimate_buffer_cents field, NOT folded in here) - estimate_buffer_cents: Refundable upfront estimate for tax and shipping that isn't final yet, added on top of shipping. Charged now and reconciled by a refund after the retailer confirms the final cost - delivery_days: Selected shipping option delivery range as { min, max } when available - delivery_estimate: Display delivery estimate such as "6 days" when available - total_cents: retailer_price_cents + shipping_estimate_cents + estimate_buffer_cents (buffer-inclusive, fee-exclusive) - service_fee_cents: Service fee in cents (order-level — added once per order, not per line; min $0.50, no maximum) - checkout_total_cents: Final all-in total charged by POST /api/order (goods + service fee + order-processing fees). The ONE price to show the customer — identical for Lightning and card for buffered quotes like this one (exact Shopify quotes are the exception: Lightning charges less — see Exact Shopify Quote); any unused portion above the retailer's final cost is refunded after the order settles - currency: "USD" - product_url: Retailer product URL - url: Retailer product URL - thumbnail: Product image URL - images: Array of product image URLs (primary image first; may be empty) — the full product image gallery - is_prime: Whether the selected offer is Prime-eligible (Amazon only; false otherwise) - variants: Channel3 variant options and a zinc_variant_hint array when available - variants_unknown: true when the data source reported no variant family — the retailer page may still have size/color options. Pass the buyer's choice as variant anyway: it is accepted unvalidated, stored in the quote, and selected at checkout - Products with a real variant choice (a dimension with more than one value) cannot be quoted unbound: the request 422s with the available options — re-quote with variant=[{"label","value"}] naming every dimension - quote_token: Price quote token (valid 30 minutes) — include this on the matching product line in POST /api/order - quote_expires_at: When the quote expires (ISO 8601) Example: GET /api/catalog/2101922242?retailer=walmart&zip=30312 GET /api/catalog/n4RTalM?retailer=nordstrom&zip=30312 ### Quote by URL (FREE) GET /api/catalog/quote The quote endpoint for product lines whose identity is a retailer product URL rather than a first-class product ID: Shopify-merchant items and cross-retailer allowlist items (target, bestbuy, homedepot, lowes, costco, wayfair, macys, kohls, staples). Every such search result carries a ready-made quote_url pointing here — follow it verbatim. Shopify quote_urls are signed capabilities binding merchant, product, listing price, and an expiry; they cannot be constructed by hand, and an expired one means search again and use a fresh quote_url. Query parameters for cross-retailer results (also constructible by hand): - url (required): Percent-encoded product URL from the search result - q (required): The original search query — the quote is minted by re-running the search and matching the URL, so the price is always fresh, never replayed - zip (optional): Delivery ZIP code to associate with the quote Query parameters for Shopify-merchant results (baked into quote_url; not hand-constructible): - source=shopify and quote={signed token} - zip (optional): Delivery ZIP hint (the buffered Shopify total itself is ZIP-independent) Response: JSON object { product, _note } where product carries the same fields as Quote One Product, including checkout_total_cents (the ONE price to show the customer), quote_token (valid 30 minutes), and quote_expires_at. The product_id is the product URL — always echo the product_id from THIS response (not the search result) on the matching line in POST /api/order. Shopify totals include a refundable buffer standing in for the store's shipping/tax; any unused portion is refunded to your refund_destination after the order. Errors: 404 when the URL no longer appears in fresh search results (prices may have changed — search again and follow a fresh quote_url); 400/expired-token errors for missing or expired Shopify quote tokens; 429 when the per-IP Shopify quote rate limit is exceeded. Example: GET /api/catalog/quote?url=https%3A%2F%2Fwww.target.com%2Fp%2F...&q=espresso+machine&zip=30312 ### Exact Shopify Quote (FREE) POST /api/catalog/quote Upgrade a buffered Shopify quote to an address-exact total — item + shipping + tax priced by the merchant's own checkout — so the buyer is charged the real total instead of a blind buffer. Optional: the buffered quote from quote_url always works; prefer this when you already know the shipping address. Required JSON body: - quote (string): The signed Shopify quote token — the value of the "quote" query parameter inside the search result's quote_url - shipping_address (object): Same shape as POST /api/order's shipping_address The address travels in the body rather than the URL so buyer PII stays out of request logs. Response: JSON object { product, _note } with the same quote fields as Quote by URL. total_cents is the merchant's exact checkout total (item + shipping + tax) with no quote buffer or safety margin; checkout_total_cents adds the standard service fee plus order-processing/payment fees and is the all-in CARD charge. A Lightning/wallet payment of an exact quote is charged less — the true total plus service fee and the flat order-processing fee, with no card-processing fees — so the authoritative wallet figure is the order preview's charge_amount_cents (and the 402 challenge amount matches it). If the store's price rises between quote and order, the order fails and the full amount is refunded to your refund_destination. Fallback: when the merchant cannot price the address headlessly, the response carries the ordinary buffered quote plus an exact_quote_unavailable field naming the reason — checkout still proceeds with the buffered total. Errors: 400 for an invalid or incomplete shipping_address; 429 when the per-IP Shopify quote rate limit is exceeded. ### Preview Order (read-only) POST /api/order/preview Price a whole basket as one order WITHOUT paying. Not L402-protected and it does NOT redeem the quote tokens — the same tokens still place the order afterward. Use this to show the customer the real order total before checkout: the service fee is ORDER-LEVEL (charged once per order, min $0.50, no maximum), so summing per-line quote fees over-counts a multi-item basket — always take service_fee_cents from this response, never compute it per line. Required JSON body: - retailer (string): same as POST /api/order - products (array): the same 1-10 same-retailer lines you would place, each with product_id, quantity, and quote_token Response (200): - products (array): per-line breakdown; each line's retailer_price_cents, shipping_estimate_cents, and estimate_buffer_cents are per-line (per-unit × quantity) and sum to quoted_line_total_cents. Also quoted_unit_total_cents and quote_expires_at - service_fee_cents: the single order-level service fee (once per order) - shipping_estimate_cents, estimate_buffer_cents: order aggregates (sum of the per-line values) - quoted_total_cents: goods total (buffer-inclusive, fee-exclusive); order_total_cents: quoted_total_cents + service_fee_cents (internal component, do NOT show as the price); charge_amount_cents: the all-in total the POST /api/order Lightning (L402) challenge will charge — the price to show a Lightning payer. For buffered quotes this equals the card all-in (one price on both rails); for exact Shopify quotes the Lightning charge is lower (no card-processing fees) and a card payment charges the quote's checkout_total_cents instead - Bad line → 422 { error, error_code, quote_token } (error_code: QUOTE_NOT_FOUND | QUOTE_EXPIRED | QUOTE_REDEEMED | PRODUCT_MISMATCH | RETAILER_MISMATCH | VARIANT_MISMATCH | DUPLICATE_QUOTE_TOKEN | MIXED_RETAILER) Example: POST /api/order/preview ### Create Order POST /api/order Place an order with a supported retailer. L402-protected — you must pay a Bitcoin Lightning invoice before the order is placed. Required JSON body: - retailer (string): Supported values: "amazon", "walmart", "nordstrom", "gap", "oldnavy", "bananarepublic", or a Shopify merchant's domain exactly as returned by the search result's retailer field (e.g. "flightcoffeeco.com") - products (array): 1 to 10 same-retailer product lines, each with: - product_id (string): The retailer product ID (e.g. "B0CX23V2ZK" for Amazon, "2101922242" for Walmart, "n4RTalM" for Channel3 retailers, or the product URL for Shopify-merchant items — always echo the product_id from the quote response) - quantity (number): Integer from 1 to 100. Use one product line per distinct product; set quantity to buy multiple units of the same product. Legacy top-level quote_token supports quantity 1 only. - quote_token (string): Price quote from /api/catalog/{productId}?retailer={retailer}. Required on each product line. - variant (optional array): Zinc variant hints as { label, value } pairs when a retailer product page needs a specific option selected - shipping_address (object): - first_name (string) - last_name (string) - address_line1 (string) - address_line2 (optional string) - postal_code (string): Delivery postal/ZIP code (e.g. "30301") - city (string) - state (string) - country (string, e.g. "US") - phone_number (string, required): E.164 format (e.g. "+14045551234") - quote_token (string, legacy): Accepted only for exactly one product line when that line omits quote_token. - refund_destination (optional string): A Lightning Address, LNURL, BIP353 identifier, or BOLT 12 offer. Every automatic Lightning refund for this order is paid here: the full amount you paid if the order fails after payment, the unused estimate buffer after a successful order settles, and wallet-funded return credits. Strongly recommended on EVERY order — without it these refunds are parked unpaid instead of sent. - shipping (optional object): Shipping preferences - is_gift (optional boolean): Whether this is a gift order - gift_message (optional string): Gift message Response: JSON object containing: - order_id: The order request_id (use for all lifecycle endpoints) - status: Order status - retailer: The retailer - quoted_total_cents: The price you were quoted (product + shipping) - service_fee: The unhuman service fee in cents - product_count: Number of product lines in the order Refunds: if the order fails after you have paid, the full amount you paid is automatically refunded to your refund_destination via Lightning. After a successful order, any unused portion above the retailer's final cost (the estimate buffer) is refunded to the same destination once the retailer confirms the final cost. No action is required on your part. Forgot refund_destination on a rejected order? The refund is parked, not lost, and your L402 credential stays valid. Resubmit the exact same order request — same lines, same quote tokens, same Authorization header — with refund_destination added: the order is not placed again; the parked refund is claimed and paid out to the destination you supplied. For any other parked refund (e.g. an unused buffer on a successful order with no destination), contact support. ### Get Order Status GET /api/order/{orderId} Check the status of an existing order. Requires the same L402 credential used to create the order. Response: JSON object with order details including: - request_id: Order ID - status: Order status - tracking_numbers: Tracking numbers when available - items: Item-level statuses when available - delivery_dates: Delivery date estimates when available ### Get Order Tracking GET /api/order/{orderId}/tracking Get a concise, owner-safe tracking view for an order. Requires the same L402 credential. Response: JSON object containing: - order_id: The order ID - status: Raw order placement status - state: Fulfillment state for display ("ordered", "shipped", or "delivered"), derived from per-item status - estimated_delivery_date: ISO 8601 estimated delivery timestamp, or null when unavailable - shipping_providers: Array of owner-safe carrier labels ({ carrier, tracking_url }). Includes carrier-only/non-public providers such as Amazon Logistics as { carrier: "Amazon", tracking_url: null }. - tracking: Array of public carrier links ({ carrier, tracking_url }). Carrier-only/non-public tracking (e.g. Amazon Logistics) is omitted. - items: Array of ordered items ({ title, quantity, product_url }) - destination: Region-level drop-off location ({ city, state, country }) ### Get Order Events GET /api/order/{orderId}/events Get tracking and status updates for an order. Requires the same L402 credential. Response: JSON object containing order event data. Fields may include: - order_id: The order ID - events: Array of order event objects ### Cancel Order POST /api/order/{orderId}/cancel Request cancellation for an order. Requires the same L402 credential. Required JSON body: - refund_destination (string): Lightning Address, BIP353 identifier, or BOLT 12 offer for any follow-up refund Important: - Cancellation is best-effort. - Cancellation is only possible while the order is still waiting in the queue. - Orders that have already started or completed cannot be canceled. - The refund destination is stored with the order for any later manual or automated refund handling. Response: JSON object containing: - order_id: The order ID - cancelled: true when the cancellation request was accepted - refund_destination: The persisted refund destination for this order ### List Returns GET /api/order/{orderId}/returns List returnable items and existing return requests for an order. Lightning orders use their L402 credential; card-funded Connect orders use their order-scoped Seam authorization. Response: JSON object containing: - order_id: The order ID - zinc_order_id: The live Zinc order ID used for the return - returnable_items: Array of returnable order items ({ order_item_id, quantity, returnable_quantity, status, product_url, title, refund_cents }) - returns: Array of existing return requests, each with return_request_id, status, reason, label_urls, merchant_return_id, and refund status ### Request Return POST /api/order/{orderId}/returns Create a return request with the same rail-specific order authorization used by List Returns. Required JSON body: - reason (string): One of "damaged", "not_delivered", "empty_box", "wrong_item", "defective", "not_as_described", "wrong_size", "no_longer_needed", "forced_cancellation", or "other" - notes (optional string): Extra return context, up to 2000 characters - items (optional array): Each item is { order_item_id, quantity }. If omitted and the order has exactly one returnable item, that full item is returned. If the order has multiple returnable items, items is required. Response: The created return request with: - return_request_id: Zinc return request ID - status: Return lifecycle status ("open", "approved", "denied", or "credited") - label_urls: Return shipping labels when Zinc approves the return - merchant_return_id: Merchant RMA/reference when available - refund: A method-discriminated refund state. Wallet-funded orders use method "wallet" and pay the returned item amount over Lightning only after Zinc marks the return "credited". Card-funded orders use method "card"; Zinc owns any eligible refund back to the original card, and unhuman never sends a Lightning payout for them. The service fee is not refunded. ### Get Return Status GET /api/order/{orderId}/returns/{returnRequestId} Refresh and return one return request. Requires the same rail-specific order authorization. Use this endpoint after creating a return to check whether Zinc has approved it, produced a label, denied it, or credited the returned item. When status is "approved", show the label_urls/RMA to the customer without claiming a refund completed. A wallet-funded "credited" return enters the Lightning payout flow. A card-funded "credited" return is an operational anomaly requiring support review; it is not proof that the original card was refunded. ## L402 Payment Flow 1. POST /api/order with your order JSON body 2. Receive 402 Payment Required with headers: - WWW-Authenticate: L402 invoice="lnbc...", macaroon="..." 3. Pay the Lightning invoice using any Lightning wallet 4. Retry the same POST with header: - Authorization: L402 {macaroon}:{preimage} 5. Order is created and confirmation returned Pricing: quoted total + 1% service fee (min $0.50, no maximum) + order-processing fees — the preview's charge_amount_cents. Buffered quotes charge one identical all-in price on every rail; exact Shopify quotes charge Lightning the merchant's true total plus the service fee and flat order-processing fee (no card-processing fees), while a card payment charges the quote's checkout_total_cents. Any unused portion above the retailer's final cost is refunded to your refund_destination after the order settles. ## Lifecycle Authentication IMPORTANT: The L402 credential from your successful order creation is your long-term access credential for that order. After creating an order, persist your Authorization header value and replay it for all lifecycle calls: - GET /api/order/{orderId} - GET /api/order/{orderId}/tracking - GET /api/order/{orderId}/events - POST /api/order/{orderId}/cancel - GET /api/order/{orderId}/returns - POST /api/order/{orderId}/returns - GET /api/order/{orderId}/returns/{returnRequestId} Missing or wrong credentials will return 401 or 403. ## Error Responses - 400: Invalid request body or expired/invalid quote - 401: Missing Authorization header on lifecycle routes - 402: Payment required (L402 challenge) - 403: Credential does not match order - 404: Order not found - 422: The cancellation or return request was rejected - 503: Service temporarily unavailable - 504: Upstream timeout ## Contact - Email: support@unhuman.store - Website: https://unhuman.shopping