# DW store endpoints that exist today (updated 2026-11-14) A GET (or a POST with no action) on any path below returns its list of actions. Nothing here was invented; the shapes match the live help on each path. If a call is not here, do not assume it exists. ## Ground rules - POST JSON with Content-Type: application/json, and always include the trailing slash (/store/cart/, not /store/cart). - Who you are is decided like this: - Public: no session, on the store's own domain. The brand comes from the Host header and is never sent or shown. - Customer: the HttpOnly cookie set by /customer_session/ or /end_user_account_setup/ on that exact host. It lasts 30 days. Apex and www are separate sign-ins. - Staff: JSON session plus brand from /session/, which is emailed and never returned over HTTP. Store writes need SUPER_ADMIN, STORE rights on the brand, or EDITOR of a domain the company owns. Never put a session or brand code in a page, a file or this kit. - Errors come back as { "ok": false, "error": "text" } with an HTTP status: 400 bad input, 401 not signed in, 403 no right, 404 not found (also used for other people's rows), 409 conflict (stock, already checked out, duplicate), 429 rate limit, 501 not switched on. - Money is USD strings with 2 decimals ("22.00"). There are no other currencies. ## Public catalog (no sign-in, on the store domain) | Path | Body | Answer | |---|---|---| | /store/products/ | {"action":"list","limit":1-100,"offset":0,"category_id"?, "company_id"?} | {ok, products:[{product_id,name,slug,price,currency,active,default_variant_id}]}. Active only. No stock and no images in the list. | | /store/products/ | {"action":"read","slug":"x"} or {"action":"read","product_id":"…"} | {ok, product:{…card, variants:[{variant_id,name,price,stock(null=not tracked),position,description}], descriptions, images:[{image_id,file_id,position,description}], categories, company_ids}} | | /store/products/categories/ | {"action":"list","company_id"?} | {ok, categories:[{category_id,company_id,parent_id,name,description(JSON object),position}]}. Category images are not included. | | /store/search/ | {"q":"tea","limit"?} (2+ characters) | {ok, q, products:[card…]}. Matches the name or description. | | /files/ | {"action":"read","id":"FILE_ID"} | For a PUBLIC file, no session is needed. Returns {ok, file:{id,file_name,file_path,file_type,created,public_private,file_data(base64)}}. tools/build.py uses this to turn an image file_id into its URL //. | ## Cart (public or customer) /store/cart/. The cart id is a server-generated, unguessable id. An anonymous cart is reachable by anyone holding the id. Once a signed-in customer touches it, only that customer (and staff) can reach it. | action | Body | Notes | |---|---|---| | add | variant_id, quantity(1-999), optional cart_id, notes, company_id | Without cart_id, a new cart is created. company_id is needed only if several companies sell the product. One line per variant, and adding again raises the quantity. Errors: 409 only N in stock, 404 variant not found or not for sale, 409 already checked out, 400 cart belongs to another seller. | | read | cart_id | | | update | cart_id, variant_id or item_id, quantity(0 removes) | | | remove | cart_id, variant_id or item_id | | | delete | cart_id | Refused once the cart has been checked out. | | mine | (customer) | {carts:[cart…]}: open carts of the signed-in customer, up to 20. | Every cart answer is {ok, cart:{cart_id, company_id, signed_in, items:[…], subtotal, currency, invoice_id}}. invoice_id is set once the cart has been checked out. ## Customer sign-up and sign-in (on the store domain) /end_user_account_setup/ is for new customers. It is limited to US networks (no Tor, VPN or datacenter addresses), allows 1 code per email per minute and 3 per hour, and 5 sign-ups per network per hour. | action | Body | Answer | |---|---|---| | request_code | email | {ok, id, emailed, expires_in_minutes:30, next}. A 6-character code is emailed and never returned. If the email already has an account, it gets a "you already have an account" email instead, with the same JSON. | | verify | id, confirmation_id (or code) | {ok, verified, signed_in, session_days:30, next:"profile"}. Creates the person and email, and sets the cookie. Returns 409 if the email already has an account. 5 wrong codes cancel the id. | | profile | any of first_name, middle_name, last_name, company_name, telephone{phone_number,line_type}, shipping{…}, billing{…} | {ok, profile_updated, company_id?, shipping_address_id?, billing_address_id?}. Works only for customers who verified through this endpoint. | | status | | {ok, verified, person:{first_name,middle_name,last_name}, has_company, has_shipping, has_billing, has_telephone} | | delete_account | confirm_email (must match the signed-in account), own session only | Deletes the sign-in email, phone numbers, saved addresses and open carts (history rows kept), ends every session on this store, and signs the customer out. Orders and invoices are kept for the store's records. Refused (409) while an order is waiting for payment: the store must cancel it first. | /customer_session/ is for existing customers. | action | Body | Answer | |---|---|---| | request_code | email | Always {ok, id, message}. The code is emailed only if the email has a person on this brand. Limits: 1 per minute and 5 per hour. 5 wrong codes kill the id, and the code lasts 15 minutes. | | verify | id, code | {ok, signed_in:true, session_days:30} and sets the cookie. | | whoami | | {ok, signed_in:false} or {ok, signed_in:true, first_name, last_name, email, expires} | | logout | | {ok, signed_out} | ## Addresses: /address/ Customers use the cookie and can touch only their own (PRSN) addresses. Staff use session and brand with CRM rights, plus owner_type PRSN or CMPNY and owner_id. | action | Body | Answer | |---|---|---| | create | line_1, city, state, zipcode required; line_2, line_3, country (default US) and address_type Shipping (default), Billing or Other are optional | {ok, address_id} | | list | optional address_type | {ok, addresses:[…]} | | read / update / delete | address_id (+ fields) | Past orders keep their own copy in invoice_address. | ## Checkout, quotes and orders | Path | Body | Answer | |---|---|---| | /store/checkout/ | {"action":"quote","cart_id", "shipping_address_id"? or "state"?} | Public or customer. Returns subtotal, flat-rate shipping, Hawaii GET and total, computed exactly as checkout will. Nothing is saved. Use shipping_address_id when signed in, or state (for example "HI") for an estimate. | | /store/checkout/ | {"action":"checkout","cart_id","shipping_address_id","billing_address_id"?} | Customer. {ok, invoice:{invoice_id, cart_id, status:"PENDING", subtotal, tax, shipping, total, paid, balance, currency, lines[], addresses[], payments[], shipments[]}, …}. Writes a sale movement for each tracked line. Emails a confirmation to the customer and a copy to the store. No payment is taken. Both addresses must belong to the signed-in customer. A cart can be checked out once. | | /store/invoice/ | {"action":"list"} | {ok, invoices:[{invoice_id,company_id,status,total,currency,date_created}]} (own only) | | /store/invoice/ | {"action":"read","invoice_id"} | {ok, invoice:{…as above}} | | /store/invoice/payment/ | {"action":"create","invoice_id","amount","method":"CARD" or "PAYPAL"} | Customer: recorded PENDING, no charge is made. | | /store/invoice/payment/ | {"action":"paypal_start","invoice_id"} or {"action":"paypal_capture","invoice_id","paypal_order_id"} | Customer. Answers 501 until the platform PayPal flag and the store's PayPal flag are both on. | | /store/invoice/payment/ | {"action":"list","invoice_id"} | {ok, payments:[…]} | | /store/shipment/ | {"action":"read","shipment_id"} or {"action":"list","invoice_id"} | own orders | ## Staff calls (JSON session + brand; tenant AI runs them, never the browser) | Path | action: required fields | |---|---| | /store/settings/ | read: company_id (effective settings: defaults plus this company's overrides). update: company_id plus any of shipping_flat, free_shipping_over, hi_get_rate (percent, e.g. "4.712"), tax_shipping (true/false), store_email. Send null to go back to the default. Needs STORE UPDATE or SUPER_ADMIN. | | /store/inventory/ | receive: variant_id or product_id (default variant), quantity (1+); optional location_id, unit_cost, notes, lot_id, serial, bin. adjust: variant_id or product_id, and either quantity (signed, not 0) or counted (physical count). stock: variant_id or product_id. rebuild: recomputes balances from the movement history. | | /store/inventory/locations/ | list: company_id (optional include_inactive). create: company_id, name (optional type, is_default). update: location_id plus name, type, is_active or is_default. A default location "Main" is created the first time one is needed. The default cannot be deactivated until another is made the default. | | /store/inventory/movements/ | list: company_id (required unless brand-wide rights); optional variant_id, product_id, location_id, movement_type, reference_type, reference_id, limit, offset. Newest first. | | /store/inventory/balances/ | list: company_id; optional location_id, product_id. Cached on_hand / reserved / available. | | /store/product/categories/ | create: company_id, name (+ parent_id, position, description JSON). update, delete, link, unlink | | /store/product/ | create: company_id, name, price (+ slug, active, variant_name, variant_price, stock, description) returns {product_id, slug, variant_id, description_id}. If stock is passed, it becomes opening inventory at the default location. update, delete, claim, unclaim | | /store/product/variants/ | list (public): product_id. create: product_id, name (+ price, stock, position, description). update / delete. Do not use update to change tracked stock once inventory is in use. | | /store/product/description/ | create: product_id, description (+ used_location, default "main"). update, delete | | /store/product/images/ | create: id_type PRODUCT, PRODUCT_VARIANT or CATEGORY, associated_id, file_id (+ position, description JSON). update, delete | | /files/ | create: file_name, file_path, file_type, public_private:"PUBLIC", file_data(base64) returns {id}. update, read, delete, search | | /store/invoice/ | list (+ company_id, status). read. update: invoice_id + tax, shipping (only while PENDING; recomputes the total) or status PENDING, VOIDED or REFUNDED. VOIDED does not automatically restore stock. | | /store/invoice/payment/ | create: invoice_id, amount, method PAYPAL, CARD, CASH or OTHER (+ status COMPLETED by default, or PENDING; + provider_ref). The invoice becomes PAID when completed payments cover the total. update: payment_id + status COMPLETED, FAILED or REFUNDED (+ provider_ref) | | /store/shipment/ | create: invoice_id, items[{invoice_product_id, quantity}] (+ carrier, service, tracking_number, tracking_url, shipping_cost, status). update, delete, read, list | | /crm/ | create_person, update_person, delete_person, create_company, update_company, delete_company. There is no read or list action. | | /relation/ | create: person_id, company_id. update, delete, read (by id, person_id or company_id) | | /address/ | as above with owner_type and owner_id | ## Page paths a tenant must not use On a store domain these paths are app folders, so a GET returns API help JSON (or, on the app host, an HTML guide) instead of a page: /store/cart/, /store/checkout/, /store/invoice/, /store/product/, /store/products/, /store/search/, /store/shipment/, /store/settings/, /store/inventory/, /store/kit/, plus /address/, /customer_session/, /end_user_account_setup/, /files/, /session/, /crm/, /relation/, /_dw/. Use files such as /store/cart.html, /store/checkout.html and /store/account.html. Never use a product slug that matches one of those folder names.