API Reference
Everything a Custom Website storefront needs to call ExiusCart directly — products, checkout, payment, customer accounts, and more. No ExiusCart login is required to read this page or to call any endpoint marked No auth.
https://api.exiuscart.com/api/v1Every path below is appended directly to that base URL. Every endpoint is scoped to your store either by a {shop_slug} in the path, or by an opaque token (order number + email, a review link, a download code) — never by an ExiusCart seller login. CORS is open, so you can call these directly from the browser.
Customer token and Optional customer token mean a JWT from Customer Auth below, sent as Authorization: Bearer <token>. Gateway webhook only means your storefront should never call it — it exists purely for Stripe/PayPal/Whop to call directly.
Categories
Your storefront’s category tree, for nav menus and category pages.
/public/store/{shop_slug}/categoriesNo authStorefront category tree, for nav/grid display.
Query: channel (default "custom")[{ id, name, slug, icon_url, parent_id }]Products
Listing, detail, and reviews for products you’ve enabled on your Custom Website channel. A product only appears here once the seller has explicitly turned it on for this channel — nothing shows by default.
/public/store/{shop_slug}/productsNo authProduct listing for a grid/search page.
Query: search, featured, trending, category (slug)Array of products: id, name, slug, description, currency, price, compare_at_price, in_stock, product_type, affiliate_url, affiliate_cta_text, faq[] ({question, answer}), shipping_note, shipping_steps[], seo_title, meta_description, seo_keywords[], highlights[] ({icon, label}), quantity, images[], video_url, videos[], tags[], variants[], category_id, category_slug, category_ids[], category_slugs[], quantity_tiers[], avg_rating, review_count, view_count, units_sold, custom_fields/public/store/{shop_slug}/products/{slug}No authSingle product detail page (PDP). Increments the product’s view count on each call.
Same shape as the listing, single object. category_ids/category_slugs carry every category the product belongs to — a product can be filed under more than one. product_type is "physical" | "digital" | "affiliate" — for "affiliate", render affiliate_cta_text (fallback "Buy Now") as a link straight to affiliate_url instead of an Add to Cart button; the checkout endpoint below rejects affiliate products server-side if one is submitted anyway. description is rich HTML from the dashboard editor — render it with dangerouslySetInnerHTML (or equivalent), not as plain text. It can include a seller-inserted pill row (a <div> of <span> tags for things like skills/features) that carries its own inline style="..." attributes rather than relying on any ExiusCart stylesheet — if you sanitize this HTML before rendering, make sure your sanitizer’s allowlist keeps the style attribute on div/span, or those pills will silently lose their styling and show as plain text. faq is an optional seller-written Q&A list to render below the description; shipping_note is an optional free-text shipping/returns blurb (physical products only); shipping_steps is an optional ordered string array — render as a connected arrow-flow, falling back to shipping_note when empty; seo_title and meta_description are optional, written for the Google result: when set, use them as-is for the page <title> and <meta name="description">; seo_keywords is an optional string array the seller is targeting — use it to build your own meta title/description and schema.org Product structured data, it is not meant to populate a raw <meta name="keywords"> tag; highlights is an optional array of {icon, label} short facts meant to render under the price (e.g. "1 Year Access", "Delivered by Email") — never sent for affiliate products — where icon is always one of a fixed set: clock, calendar, mail, truck, package, shield, download, check-circle, refresh-cw, star, gift, tag. All render only when non-empty, no fallback copy./public/store/{shop_slug}/products/{slug}/reviewsNo authApproved reviews for one product.
[{ id, customer_name, country, rating, comment, photo_url, photos[], video_url, verified_buyer, pinned, seller_reply, reply_from, submitted_at, source_label }], pinned first. customer_name is null when the buyer hid their name: show their country instead (2-letter code, e.g. with a flag). photos has every photo (up to 5); video_url is the buyer’s short video. seller_reply is the store’s public answer: show it under the review as “Reply from {reply_from}”. source_label is set on reviews imported from another marketplace./public/store/{shop_slug}/products/{slug}/questionsNo authAnswered shopper questions for one product (Questions & answers under the reviews).
{ questions_enabled, questions: [{ id, question, answer, asker_name, answered_by, answered_at }] }. When questions_enabled is false, hide your "Ask a question" button./public/store/{shop_slug}/products/{slug}/questionsNo authA shopper asks about a product.
Body: name, email, question (5-500 characters){ status: "received" }. The store is emailed; the shopper gets the answer by email, and it shows in the list above once the store publishes it. 400 when the store turned questions off. 5 calls a minute./public/store/{shop_slug}/products/{slug}/rating-schemaNo authReady JSON-LD for Google star ratings. Put json_ld in a <script type="application/ld+json"> on the product page.
{ has_rating, json_ld }. json_ld is a schema.org Product with aggregateRating and up to 5 reviews. Only the shop’s own buyers’ reviews count (Google does not allow reviews copied from other sites), so has_rating is false until the store has some. If your page already outputs Product data, copy aggregateRating and review into it instead of adding a second Product./public/product/{barcode}No authProduct + active reservations lookup for a QR/barcode page. Not shop_slug-scoped.
name, sku, barcode, price, currency, stock, reserved, available, shop_name, image_url, category, reservations[]Reviews
Submitting a review from a post-delivery email link, and the embeddable review widget.
/public/review/{token}No authReview-request landing page data, from the link in a post-delivery email.
product_name, product_image, shop_name, customer_name, already_submitted, rating, comment, allow_photos, allow_videos, max_photos, reward ({ text, for: "any" | "media" } when the store gives a thank-you code), reward_sent/public/review/{token}/submitNo authSubmit a review via the request-link token.
Body: rating (1–5), comment?, name? (the name shown with the review), photo_urls? (up to 5, from the photo upload below), video_url? (from the video upload){ submitted: true, reward_sent }. reward_sent: the buyer was emailed a thank-you discount code./public/review/{token}/photoNo authUpload a photo for that review.
multipart file (≤ 8MB, jpg/jpeg/png/webp){ url }/public/review/{token}/videoNo authUpload one short video for that review, when the store takes video reviews.
multipart file (≤ 60MB, mp4/mov/webm){ url }/public/products/{product_id}/reviewsNo authApproved reviews for the embeddable widget, by raw product id.
{ avg_rating, count, reviews: [same shape as the store reviews above], questions_enabled, questions: [...], rating_schema }. rating_schema is aggregateRating + review for Google (null until the store has its own buyers’ reviews)./public/products/{product_id}/questionsNo authAsk a question by raw product id (what the widget uses).
Body: name, email, question{ status: "received" }. 5 calls a minute./widget/reviews.jsNo authStatic embeddable script — drop it in and it auto-mounts any element with data-exiuscart-reviews.
On the element: data-product-id (required); data-questions="off" hides Questions & answers; data-schema="on" adds Google star-rating JSON-LD to the page (data-product-name sets the product name in it). Shows photos, videos and the store’s replies.Customer Auth
Signup/login for shoppers on your storefront. Returns a customer JWT (type: "customer") that’s only valid on the customer-scoped endpoints below — it’s a different token type than a seller session and can’t be used to access the ExiusCart dashboard.
/public/store/{shop_slug}/auth/signupNo authCreate a storefront customer account.
Body: name, email, password (min 8 chars), birthday? ("MM-DD"; a full date works, only month and day are kept), marketing_consent? (true only if they ticked an unticked "email me offers" box), visitor_id? (the id you send to /track, so their browsing joins them){ token, customer: { id, name, email, birthday } }. 409 if the email already has an account, 422 for a short password or a birthday that is not a real day. Making an account is not a marketing yes: without marketing_consent: true they get no marketing emails./public/store/{shop_slug}/auth/loginNo authLog an existing customer in.
Body: email, password{ token, customer: { id, name, email } }/public/store/{shop_slug}/auth/login-codeNo authSign in without a password: email the shopper a 6-digit code.
Body: email, create_account? (true makes the account when the email is new), name?{ status: "code_sent", sent_to, expires_in } every time, whether or not the email has an account, so it never reveals who shops at the store. Guest-checkout customers can sign in this way too, and see their past orders. A second request within 45 seconds is not re-sent. Codes last 10 minutes. 6 calls a minute./public/store/{shop_slug}/auth/login-code/verifyNo authSwap the emailed code for a login token.
Body: email, code{ token, customer: { id, name, email, birthday, has_password } }, the same token password login gives. Codes are single use; 5 wrong tries cancel the code. 400 for a wrong or expired code.Send the token as Authorization: Bearer <token> on any endpoint below marked "customer" or "optional". A 401 means the session ended (expired, or the password was changed on another device): send the customer back to login.
Orders, Addresses & Returns
The shopper’s own order history, “buy it again”, saved addresses and returns. Every endpoint acts on the customer in the token, and only on their own orders.
/public/store/{shop_slug}/account/ordersCustomer tokenOrder history, newest first.
Query: skip?, limit? (up to 50){ orders: [{ order_number, status, payment_status, total, items_count, created_at, tracking_number, carrier }], total, currency }/public/store/{shop_slug}/account/orders/{order_number}Customer tokenOne order, its parcels and its returns.
{ …order, items: [{ order_item_id, product_name, quantity, unit_price, variant_size, variant_color, returnable_quantity }], shipments: [{ items, carrier, tracking_number, tracking_url, created_at }], returns: [...], return_window: { can_return, why_not, until } }. An order can ship in more than one parcel; each has its own tracking./public/store/{shop_slug}/account/orders/{order_number}/reorderCustomer token“Buy it again”: the cart lines for this order at today’s prices and stock.
{ items: [{ product_id, variant_id, product_name, slug, image_url, quantity, ordered_quantity, unit_price, price_changed, limited_by_stock }], unavailable: [{ product_name, reason }], currency }. Nothing is ordered here: add items to your cart, then checkout as usual./public/store/{shop_slug}/account/addressesCustomer tokenSaved addresses, default first.
{ addresses: [{ id, label, name, phone, line1, line2, city, region, postal_code, country, is_default }] }/public/store/{shop_slug}/account/addressesCustomer tokenSave an address. The first one becomes the default.
Body: line1, label?, name?, phone?, line2?, city?, region?, postal_code?, country? (2-letter code), is_default?{ addresses, id }. Up to 20 per customer. 422 for a country that isn’t a 2-letter code./public/store/{shop_slug}/account/addresses/{id}Customer tokenChange an address, or make it the default (is_default: true).
{ addresses }/public/store/{shop_slug}/account/addresses/{id}Customer tokenDelete an address. If it was the default, the oldest one left takes over.
{ addresses }/public/store/{shop_slug}/return-policyNo authThe store’s return rules and the reasons to offer.
{ returns_enabled, return_window_days, policy, reasons: [{ key, label }] }/public/store/{shop_slug}/account/returnsCustomer tokenStart a return.
Body: order_number, items: [{ order_item_id, quantity }], reason (a key from return-policy), note?, preferred_outcome? ("refund" | "exchange" | "store_credit"){ id, return_number, status: "requested", items, … }. Only paid, shipped orders inside the return window; never more units than were bought (minus earlier returns). The store is emailed; the shopper gets a confirmation and an email each time it moves on. 10 calls a minute./public/store/{shop_slug}/account/returnsCustomer tokenThe shopper’s returns.
{ returns: [{ id, return_number, order_number, status, reason, seller_note, refund_amount, items, created_at }] }. status: requested, approved, declined, received, refunded or cancelled./public/store/{shop_slug}/account/returns/{id}/cancelCustomer tokenCancel a return the store hasn’t handled yet.
The return, now cancelled. 400 once the store has approved or declined it.Customer Account
ExiusCart is the backend for your storefront’s customers: their profile, password and payout details are stored here, so your site keeps no customer data of its own. Every endpoint acts on the customer in the token. Never send a customer id. Errors come back as { "detail": "message" }, written to show to the shopper as-is.
/public/store/{shop_slug}/account/profileCustomer tokenThe logged-in customer’s profile.
{ customer: { id, name, email, phone, birthday }, pending_email: { sent_to, expires_at } | null }. pending_email is a new email still waiting for its code. Use it to show the "enter code" step again if the customer reloads./public/store/{shop_slug}/account/profileCustomer tokenUpdate name, email, phone or birthday. Send only the fields that changed.
Body: name?, email?, phone?, birthday? ("MM-DD"; "" removes it), current_password (required only when email changes){ customer, email_change: { status: "code_sent", sent_to, expires_at } | null }. Name and phone save straight away. A new email does NOT: ExiusCart emails a 6-digit code to the new address, and customer.email stays the old one until the code is confirmed. 400 if the password is missing or wrong, 409 if the email belongs to another account, 422 if it isn’t a valid email.Sending the same new email again sends a fresh code (use it as "resend").
/public/store/{shop_slug}/account/email/verifyCustomer tokenConfirm a new email with the code that was sent to it. Only now does the email change.
Body: { code }{ customer } with the new email. The old address gets an alert email. 400 if the code is wrong, expired (codes last 15 minutes) or nothing is waiting. After 5 wrong codes the change is cancelled and the customer must ask again. 409 if someone else took the email in the meantime./public/store/{shop_slug}/account/passwordCustomer tokenChange the password. This signs the customer out on every other device.
Body: current_password, new_password (min 8 chars){ token, customer }. Replace your stored token with this new one, because the old token stops working. 400 if current_password is wrong, 422 if the new one is too short or the same as the old one./public/store/{shop_slug}/account/payout-methodCustomer tokenWhere this customer’s wallet withdrawals are sent, plus the payout methods this store accepts.
{ payout_method: { method, method_label, account, account_name, paypal_email, changed_at, updated_at, withdrawals_unlock_at } | null, paypal_email, pending_payout_method: { method, method_label, account, account_name, sent_to, expires_at } | null, withdrawals_enabled, methods: [{ key, label, kind }] }. kind "email" means account is an email address; "text" (bank transfer) means free-form bank details. pending_payout_method is a new payout email still waiting for its code./public/store/{shop_slug}/account/payout-methodCustomer tokenSave or change payout details. Needs the current password. A new payout email (PayPal, Payoneer, Skrill, Wise) must be confirmed with a code sent to that address. Bank details save straight away.
Body: method ("paypal" | "payoneer" | "skrill" | "wise" | "bank_transfer", only ones listed in methods above), account, account_name?, current_password. Shorthand: { paypal_email, current_password } is the same as method "paypal".202 { payout_method, paypal_email, verification: { status: "code_sent", sent_to, expires_at } } when a code was sent. payout_method is still the OLD details (or null) until the code is confirmed. 200 { payout_method, paypal_email, verification: null } when saved straight away (bank details, or only the name changed)./public/store/{shop_slug}/account/payout-method/verifyCustomer tokenConfirm a new payout email with the code that was sent to it. Only now are withdrawals sent there.
Body: { code }{ payout_method, paypal_email, verification: null }. The customer gets an alert email. Changing existing details (not adding them the first time) pauses withdrawals for the store’s waiting time, shown as withdrawals_unlock_at. Show it to the customer. 400 if the code is wrong, expired (15 minutes) or nothing is waiting; 5 wrong codes cancel the change.Checkout & Orders
Placing an order, guest or logged-in, and tracking it afterward.
/public/store/{shop_slug}/checkoutOptional customer tokenCreate a pending order and get back the payment gateway’s request parameters. Works for a guest (just name/email) or a logged-in customer. Stock is validated here but not yet decremented — that happens once payment is confirmed.
Body: items [{ product_id, quantity, variant_id? }], name, email, phone?, shipping_address? (plain text, or an object { name, address, city, province, zip, country_code } — send the object if you want paid orders sent to your supplier automatically), use_wallet_amount? (logged-in only), return_url? / cancel_url? (Stripe & PayPal){ order_number, total, payment: { gateway, order_id, ...gateway-specific fields — see Payment below } }/public/store/{shop_slug}/orders/{order_number}No authGuest order lookup/tracking — matched by order number + the email it was placed with.
Query: email (required){ order_number, status, payment_status, total, items: [{ product_name, quantity, unit_price, total_price, variant_size, variant_color }], created_at, tracking_number, carrier, shipped_at, estimated_delivery }. status is "pending" | "confirmed" | "processing" | "shipped" | "delivered" | "cancelled" — moves to "confirmed" automatically once payment_status becomes "paid"; every status after that is set manually by the seller from their dashboard. tracking_number/carrier/shipped_at are null until the seller marks the order shipped — treat null as "not shipped yet", not an error./public/store/{shop_slug}/payment-return/paypalNo authPayPal redirects the shopper back here after approval; this captures the payment server-to-server, then redirects onward to your site.
Query: order_number, redirect_to, token (PayPal order id)302 redirect to {redirect_to}?payment=success|failed&order_number=...Payment
Stripe, PayPal, and Whop are supported. The gateway is configured per-seller in their ExiusCart dashboard; checkout’s response tells you which one is active and what to do with it.
/public/payment-webhook/{shop_slug}Gateway webhook onlyServer-to-server only — the payment gateway calls this directly, your storefront never does. Signature-verified before anything is trusted; marks the order paid, decrements stock, credits wallet cashback, and triggers digital delivery if applicable.
Wallet
Store credit earned as cashback on paid orders, spendable at checkout. If the store is on the Growth or Scale plan and the owner turns withdrawals on, customers can also ask for their balance as money. The owner pays them by hand (PayPal, Payoneer, Skrill, Wise or bank) and marks it paid in their dashboard.
/public/store/{shop_slug}/wallet-settingsNo authThe store’s cashback rate and withdrawal rules. Use it for "5% back" labels and to decide whether to show a Withdraw button.
{ is_enabled, cashback_percent, withdrawals: { enabled, min_amount, currency, methods: [{ key, label, kind }], payout_change_hold_hours } }/public/store/{shop_slug}/walletCustomer tokenBalance and the last 50 transactions for the logged-in customer.
{ balance, on_hold, currency, withdrawals_enabled, transactions: [{ type, amount, description, payout_request_id, created_at }] }. balance is what can be spent or withdrawn now. on_hold is already taken out of it while a withdrawal waits./public/store/{shop_slug}/wallet/payout-requestsCustomer tokenAsk to withdraw part or all of the balance. The amount goes on hold straight away.
Body: { amount }. Nothing else is read. The customer, balance, currency and payout address all come from the token and the saved payout method, so extra fields are ignored.201 { id, amount, currency, method, method_label, account, paypal_email, status: "pending", reject_reason, payment_reference, can_cancel, created_at, approved_at, paid_at, rejected_at, cancelled_at }. 403 if the store has withdrawals off. 400 if there are no payout details, the amount is under the minimum or over the balance, or withdrawals are paused after a payout-details change. 409 if a withdrawal is already in progress (one at a time)./public/store/{shop_slug}/wallet/payout-requestsCustomer tokenThis customer’s withdrawals, newest first.
Array of the same shape as above. status is "pending" | "approved" | "paid" | "rejected" | "cancelled". Show reject_reason when rejected and payment_reference when paid./public/store/{shop_slug}/wallet/payout-requests/{id}/cancelCustomer tokenCancel a withdrawal that is still pending. The amount goes back into the wallet.
The updated request. 409 once the store has approved or acted on it.Spend the balance by passing use_wallet_amount in the checkout body above. It is capped server-side at the real balance, and only ever debited from the authenticated customer’s own account, never a guest email. Money on hold for a withdrawal can’t be spent. The customer gets an email at every step of a withdrawal, and the store owner gets one for each new request.
Digital Delivery
The download gate for digital products — the real file link is only ever revealed after a code sent by email is verified.
/public/download/{token}No authDownload gate landing page — reveals the product/shop name only, never the file.
{ product_name, shop_name }/public/download/{token}/verifyNo authVerify the emailed access code. Rate-limited to 10 attempts/hour/token.
Body: code{ file_url, file_name }Blog
/public/store/{shop_slug}/blogNo authPublished post list (omits the full content field, for a lighter list view).
Query: tag?[{ id, title, slug, excerpt, cover_image_url, status, published_at, author_name, tags[], cta_text, cta_url, view_count, created_at, updated_at }]/public/store/{shop_slug}/blog/{slug}No authSingle post detail. Increments view count.
Same shape, plus content.Visitor Tracking & Email Marketing
What powers the Website Traffic chart, the product funnel and the store’s Eevo Flows emails (browse abandonment, back in stock, birthday...). Keep one random visitor_id per browser (8-64 letters, digits, - or _, e.g. in localStorage) and send it on every call. Knowing someone’s email is not permission to market to them: send marketing_consent: true only when the shopper ticked an unticked "email me offers" box. Without it, a new shopper gets only the emails they asked for (like back in stock), never marketing. All of these return 404 for an unknown or inactive store, and 429 when over the rate limit.
/public/store/{shop_slug}/trackNo authOne visitor event. On every page load send ONE of: page (any non-product page, with path) or view (a product page, with product_id). Also send search (with query) and add_to_cart (with product_id). 60 a minute per IP.
Body: { event: "page" | "view" | "search" | "add_to_cart", product_id?, query?, path?, visitor_id?, email? (when signed in), marketing_consent? }{ ok: true }. 422 for an unknown event, a view/add_to_cart without product_id, or a search without query./public/store/{shop_slug}/identifyNo authThe shopper gave their email (newsletter box, checkout email field, sign-in). Ties the visitor_id to them, and their last 30 days of views and add-to-carts join their profile. 20 a minute per IP.
Body: { visitor_id, email, name?, birthday? ("MM-DD"), marketing_consent? }{ ok: true, marketing_consent, needs_confirmation }. marketing_consent is whether this person may now get marketing emails. needs_confirmation is true when the store uses double opt-in and a confirmation email was sent. 422 for a bad visitor_id, email or birthday.marketing_consent: true subscribes them to the store’s email list and keeps proof (time, IP, browser, page). false or missing never removes an earlier yes. The unsubscribe link in every email does that. On sign-out, start a new visitor_id so the next person on a shared computer is not mixed up with them.
/public/store/{shop_slug}/subscribeNo authNewsletter signup by store slug. Subscribing is the marketing yes. 20 a minute per IP.
Body: { email, name?, birthday? ("MM-DD"), visitor_id?, source? ("website_form" | "popup" | "storefront"), popup_id?, page_url? }{ status: "ok", discount_code, prize, needs_confirmation }. discount_code is the popup’s code when popup_id is one of the store’s popups (that is how a "10% off for signing up" box gets its code), else null. needs_confirmation: true means double opt-in is on, so show "check your inbox" rather than the code./public/email-list/{shop_id}/subscribeNo authThe same signup by numeric shop id (used by the popup and form widgets).
Body: { email, name?, source?, popup_id?, page_url? }As above./public/store/{shop_slug}/product-alertsNo auth"Notify me when it’s back", "Tell me if the price drops" or a wishlist save. The Back in stock / Price drop flows email them, even without marketing consent, because they asked for it. 20 a minute per IP.
Body: { product_id, email, kind?: "back_in_stock" | "price_drop" | "wishlist", name?, visitor_id?, marketing_consent? }201 { ok: true, token, kind }. Keep the token to remove the request later./public/store/{shop_slug}/product-alerts/{token}No authRemove a product alert or wishlist save.
204Privacy, for your privacy policy: ExiusCart stores the events above with the visitor_id, the page path or product, and the visitor’s country (looked up from the IP, which is then thrown away). Browsing is kept against the visitor_id for 90 days, then only as anonymous counts (deleted after about two years). A store can erase any shopper on request from Customers > Erase a shopper. That removes their profile, browsing, carts, consent history and leads, and anonymises their customer record. Orders stay for the store’s tax records, and their email stays only as "unsubscribed" so they are never emailed again.
Popups
Announcement, exit-intent, email-capture, and countdown popups the seller has built.
/public/popups/{shop_id}No authActive popups for the embed script. Note: shop_id, not slug.
{ popups: [{ id, popup_type, title, message, button_text, button_link, discount_code, image_url, delay_seconds }] }/public/popups/{popup_id}/trackNo authTrack an impression or click.
Query: event ("impression" | "click"){ ok: true }/widget/popup.jsNo authStatic embeddable script that renders the popups above.
Signup Forms & Lead Capture
Seller-built lead forms, plus a way to mirror submissions from a form that already exists on your own site.
/public/signup-forms/{shop_id}No authActive custom-built signup/inquiry forms.
{ forms: [{ id, title, description, fields[], delay_seconds }] }/public/signup-forms/{form_id}/trackNo authTrack a form impression.
{ ok: true }/public/signup-forms/{form_id}/submitNo authSubmit answers — creates a Lead in the seller’s CRM if an email field is present.
Body: { answers: { field_id: value } }{ status, success_message, discount_code }/public/capture-form/{shop_id}/submitNo authMirrors a submission of your own existing HTML form (tagged data-exiuscart-capture) into the Lead CRM, without changing how that form behaves.
Body: { fields: [{ name?, id?, type?, value? }], url? }{ status: "ok" }/widget/signup-form.jsNo authStatic embeddable script — renders custom forms and passively mirrors tagged existing forms.
Misc & Utility
/public/check-ref/{code}No authCheck whether an affiliate referral code is active.
{ valid: boolean }/public/reservation/{reservation_id}No authReservation info for a QR-code public page.
id, customer_name, customer_phone, product_name, quantity, reservation_type, status, advance_amount, notes, expires_at, shop_name, currency, product_stock, product_reserved/public/quotation/{token}No authA client views a shared B2B quotation.
quote_number, shop_name, items, subtotal, discount, tax, total, status, valid_until, and more/public/quotation/{token}/respondNo authClient accepts or rejects a quotation.
Body: { action: "accept" | "reject", name? }{ status }/shops/exchange-ratesNo authLive currency conversion rates (12h cache). Not shop-scoped — a general utility.
Query: base (default "USD"){ base, rates, cached, stale? }