# 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 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) 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 every supported store in parallel (Amazon, Walmart, Target, Best Buy, ..., every Shopify merchant, Nordstrom). 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", ...): 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 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 - 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. 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 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, max $5.00) - 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; 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 ### 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 / max $5.00), 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 POST /api/order will charge — the ONE price to show the customer, identical for Lightning and card - 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. If the order fails after payment, the full amount you paid is automatically refunded here. Strongly recommended — without it a failed order cannot be auto-refunded. - 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. No action is required on your part. ### 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. Requires the same L402 credential. 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. Requires the same L402 credential. 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: Refund status and amount. Normal returns refund the returned item amount only; the service fee is not refunded. Lightning payout happens only after Zinc marks the return "credited". ### Get Return Status GET /api/order/{orderId}/returns/{returnRequestId} Refresh and return one return request. Requires the same L402 credential. 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. When status is "credited", unhuman pays the item refund to the order's refund_destination and reports that in refund.status. ## 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: one all-in price on every rail — quoted total + 1% service fee (min $0.50, max $5.00) + order-processing fees (the preview's charge_amount_cents). Any unused portion above the retailer's final cost is refunded 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