Files
backend/docs/website-api/openapi.json
T
Alireza HassaniandCursor e0cd2327eb Treat website meta tags as label + full HTML snippets.
Public business-info now exposes metaTags as { html }, matching how admins paste complete <meta> tags in Tags & Badges.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-03 18:39:19 +03:30

3946 lines
108 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"openapi": "3.0.3",
"info": {
"title": "Meshkee Website API",
"version": "1.0.0",
"description": "Global storefront API for every Meshkee business website.\n\n**Not domain-specific.** Replace `{domain}` with the website apex (e.g. `sanihome.ir`).\n\n**Base URL:** `https://api.meshkee.com/api/v1` (or `https://api.{domain}/api/v1` if that alias is configured).\n\n**Tenant rule:** public content uses `/tenants/{domain}/...`.\n\n**Storefront vs customer dashboard:** Do **not** build login, register, OTP, checkout, or cart pages on the shop. Those live at `https://customer.{domain}` for every Meshkee site. The website mini-cart is a **local guest cart** (`meshkee-guest-cart`); Continue redirects to the dashboard. Load variants with `GET /tenants/{domain}/store-items/by-product/{productId}` — if more than one in-stock variant, list them and let the shopper pick one before add. See AI_PROMPT.md.\n\n**Shared login:** the customer dashboard writes parent-domain cookies `meshkee_customer_access_token` / `meshkee_customer_refresh_token` on `Domain=.{domain}`. The shop only **reads** them (to skip login on Continue). API calls still use Bearer. Not `POST /auth/handoff` (staff SSO into the business dashboard).\n\n**Technical specs (AI agents):** Detail endpoints may return `technicalValues` with **values only** (`fieldId` + text/option ids — **no labels**). To render label→value technical details, always call the matching `.../technical-info` endpoint and join `form.fields[].id` to `values[].fieldId`. There is no public `product-category-variation-fields` route.\n\n**Docs:** https://api.meshkee.com/docs/website"
},
"servers": [
{
"url": "https://api.meshkee.com/api/v1",
"description": "Production (central) — use this for all websites"
},
{
"url": "https://api.{domain}/api/v1",
"description": "Optional per-site alias (same backend). {domain} = website apex",
"variables": {
"domain": {
"default": "example.com"
}
}
}
],
"tags": [
{
"name": "Tenant"
},
{
"name": "SEO"
},
{
"name": "Homepage"
},
{
"name": "Categories"
},
{
"name": "Products",
"description": "Catalog products. For a specs/technical-details table on the product page, call `GET .../products/{slug}/technical-info` (or by-id). Do not rely on detail-only payloads for field labels."
},
{
"name": "User Products",
"description": "Customer marketplace listings. Prefer `GET .../user-products/by-id/{id}` for storefront pages. Detail returns `technicalValues` without labels; use `.../technical-info` for form field labels + values."
},
{
"name": "Store"
},
{
"name": "Torob",
"description": "Product API v3 for Torob. Called by Torob (not storefront JS). Nginx on the shop apex proxies POST /torob_api/v3/products. Only tenants with the store module enabled; otherwise 404."
},
{
"name": "Blogs"
},
{
"name": "Portfolios"
},
{
"name": "Comments"
},
{
"name": "Expert Reviews"
},
{
"name": "Contact"
},
{
"name": "Auth",
"description": "Used by the **customer dashboard** (`https://customer.{domain}`), not storefront UI. Do not build login/register/OTP pages on the shop — redirect to `https://customer.{domain}/login`.\n\nAPI calls use `Authorization: Bearer <accessToken>`. The dashboard writes parent-domain cookies `meshkee_customer_access_token` / `meshkee_customer_refresh_token` on `Domain=.{domain}`; the shop only reads them. Do not use `/auth/handoff` for shoppers (staff-only into the business dashboard)."
},
{
"name": "Addresses"
},
{
"name": "Cities"
},
{
"name": "Cart",
"description": "Server cart + checkout — **customer dashboard only**. Storefronts must not call these routes. The shop keeps a local guest mini-cart (`meshkee-guest-cart`); add-to-cart uses `storeItemVariantId` from `GET /tenants/{domain}/store-items/by-product/{productId}` (list variants if more than one, then add the chosen line)."
},
{
"name": "Orders"
},
{
"name": "Favorites"
},
{
"name": "Partner SMS"
},
{
"name": "Payments",
"description": "Online e-payment gateways — customer dashboard checkout, not storefront pages. Bank callbacks are nginx on the shop apex (`/meshkee/payments/{gateway}/callback`)."
}
],
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT",
"description": "Customer access JWT from customer-dashboard login/register/OTP/refresh. Storefronts do not obtain this via a shop login page — they read `meshkee_customer_access_token` if the dashboard already set it on `Domain=.{domain}`. Always send Bearer on API requests; cookies are not sent to the API."
},
"apiKeyAuth": {
"type": "apiKey",
"in": "header",
"name": "X-Api-Key",
"description": "Partner SMS API key (server-to-server only). Issued per allowlisted domain."
}
},
"parameters": {
"domain": {
"name": "domain",
"in": "path",
"required": true,
"description": "Website apex host only (e.g. sanihome.ir). No www/api/customer/business prefix.",
"schema": {
"type": "string",
"example": "example.com"
}
},
"businessId": {
"name": "businessId",
"in": "path",
"required": true,
"description": "From GET /tenants/{domain} → id",
"schema": {
"type": "string"
}
}
}
},
"paths": {
"/tenants/{domain}": {
"get": {
"tags": [
"Tenant"
],
"summary": "Resolve website domain → business",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "Business branding",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": {
"type": "string"
},
"name": {
"type": "string"
},
"nameFa": {
"type": "string"
},
"slug": {
"type": "string"
},
"domain": {
"type": "string"
},
"primaryColor": {
"type": "string",
"nullable": true
},
"defaultLocale": {
"type": "string",
"enum": [
"en",
"fa"
]
},
"logoUrl": {
"type": "string",
"nullable": true
},
"logoDarkUrl": {
"type": "string",
"nullable": true,
"description": "Optional logo for dark backgrounds; falls back to logoUrl when omitted."
},
"faviconUrl": {
"type": "string",
"nullable": true,
"description": "Tab icon URL. Dedicated favicon when uploaded; otherwise same as logoUrl."
},
"specialProductsSource": {
"type": "string",
"enum": [
"product",
"store_item"
],
"description": "Where homepage special-product carousels read from. Defaults to store_item when the store module is enabled, otherwise product."
},
"schema": {
"type": "object",
"description": "Site-wide Schema.org flags. Full jsonLd is on GET /tenants/{domain}/website/business-info.",
"properties": {
"enabled": { "type": "boolean" },
"type": {
"type": "string",
"enum": ["Organization", "LocalBusiness"]
}
},
"required": ["enabled", "type"]
}
}
}
}
}
}
}
}
},
"/tenants/{domain}/sitemap.xml": {
"get": {
"tags": [
"SEO"
],
"summary": "XML sitemap for search engines",
"description": "Sitemap **index** for this tenant. Points to `/sitemap-main.xml` (static pages + product categories) plus module child sitemaps when enabled: `/sitemap-products.xml`, `/sitemap-blogs.xml`, `/sitemap-portfolios.xml`, `/sitemap-videos.xml`, `/sitemap-instructions.xml`, `/sitemap-workshops.xml`, `/sitemap-user-products.xml` (when `customer_products` is enabled). Proxied from `https://{domain}/sitemap.xml`. Cached in Redis; refreshed after CMS writes and manifest sync.",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "Sitemap XML (urlset)",
"content": {
"application/xml": {
"schema": {
"type": "string"
}
}
},
"headers": {
"Cache-Control": {
"schema": {
"type": "string",
"example": "public, max-age=900"
}
}
}
},
"404": {
"description": "Unknown domain"
}
}
}
},
"/tenants/{domain}/sitemap-main.xml": {
"get": {
"tags": [
"SEO"
],
"summary": "Main sitemap urlset (static pages + product categories)",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "Sitemap XML (urlset)",
"content": {
"application/xml": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/tenants/{domain}/sitemap-products.xml": {
"get": {
"tags": [
"SEO"
],
"summary": "Products-only sitemap urlset",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "Sitemap XML (urlset) of published products",
"content": {
"application/xml": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/tenants/{domain}/sitemap-blogs.xml": {
"get": {
"tags": [
"SEO"
],
"summary": "Blogs-only sitemap urlset",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "Sitemap XML (urlset) of published blogs",
"content": {
"application/xml": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/tenants/{domain}/sitemap-portfolios.xml": {
"get": {
"tags": [
"SEO"
],
"summary": "Portfolios-only sitemap urlset",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "Sitemap XML (urlset) of published portfolios",
"content": {
"application/xml": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/tenants/{domain}/sitemap-videos.xml": {
"get": {
"tags": [
"SEO"
],
"summary": "Videos-only sitemap urlset",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "Sitemap XML (urlset) of published videos",
"content": {
"application/xml": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/tenants/{domain}/sitemap-instructions.xml": {
"get": {
"tags": [
"SEO"
],
"summary": "Instructions-only sitemap urlset",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "Sitemap XML (urlset) of published instructions",
"content": {
"application/xml": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/tenants/{domain}/sitemap-workshops.xml": {
"get": {
"tags": [
"SEO"
],
"summary": "Workshops-only sitemap urlset",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "Sitemap XML (urlset) of published workshops",
"content": {
"application/xml": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/tenants/{domain}/sitemap-user-products.xml": {
"get": {
"tags": [
"SEO"
],
"summary": "User products sitemap urlset",
"description": "Published customer marketplace listings. Default paths: `/user-products/{id}/{slug}` (slug from titleFa/titleEn). Included in the index when the `customer_products` module is enabled. Override path via sitemap-config `templates.userProduct`.",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "Sitemap XML (urlset) of published user products",
"content": {
"application/xml": {
"schema": {
"type": "string"
}
}
}
}
}
}
},
"/tenants/{domain}/robots.txt": {
"get": {
"tags": [
"SEO"
],
"summary": "robots.txt with sitemap reference",
"description": "Plain-text robots file pointing crawlers to `https://{domain}/sitemap.xml`. Proxied from `https://{domain}/robots.txt` on the storefront.",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "robots.txt",
"content": {
"text/plain": {
"schema": {
"type": "string",
"example": "User-agent: *\\nAllow: /\\n\\nSitemap: https://example.com/sitemap.xml\\n"
}
}
},
"headers": {
"Cache-Control": {
"schema": {
"type": "string",
"example": "public, max-age=900"
}
}
}
},
"404": {
"description": "Unknown domain"
}
}
}
},
"/tenants/{domain}/website/business-info": {
"get": {
"tags": [
"Homepage"
],
"summary": "About, contacts, addresses, social",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "Business public profile",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"id": { "type": "string" },
"name": { "type": "string" },
"nameFa": { "type": "string" },
"about": { "type": "string" },
"vision": { "type": "string" },
"logoUrl": { "type": "string", "nullable": true },
"logoDarkUrl": {
"type": "string",
"nullable": true,
"description": "Logo for dark backgrounds; null when not set."
},
"faviconUrl": {
"type": "string",
"nullable": true,
"description": "Dedicated favicon when uploaded; otherwise same as logoUrl."
},
"emails": {
"type": "array",
"items": { "type": "string" }
},
"phoneNumbers": {
"type": "array",
"items": {
"type": "object",
"properties": {
"type": { "type": "string", "enum": ["landline", "cell"] },
"number": { "type": "string" }
}
}
},
"socialMedia": { "type": "object" },
"addresses": { "type": "array", "items": { "type": "object" } },
"schema": {
"type": "object",
"description": "Site-wide Schema.org settings + ready-to-emit jsonLd (Phase A).",
"properties": {
"enabled": { "type": "boolean" },
"type": {
"type": "string",
"enum": ["Organization", "LocalBusiness"]
},
"name": { "type": "string", "nullable": true },
"nameFa": { "type": "string", "nullable": true },
"description": { "type": "string", "nullable": true },
"priceRange": { "type": "string", "nullable": true },
"sameAs": {
"type": "array",
"items": { "type": "string" }
},
"jsonLd": {
"type": "object",
"nullable": true,
"description": "Null when schema.enabled is false. Otherwise emit as application/ld+json on every public page (or at least the layout)."
}
},
"required": ["enabled", "type", "sameAs", "jsonLd"]
},
"metaTags": {
"type": "array",
"description": "Custom <meta> HTML snippets for the site <head>. Each item is { html } with a full <meta …> tag. Configured under Website → Tags & Badges (name there is an admin label only).",
"items": {
"type": "object",
"properties": {
"html": {
"type": "string",
"description": "Full sanitized <meta …> HTML tag to inject in <head>."
}
},
"required": ["html"]
}
},
"trustBadges": {
"type": "array",
"description": "Enabled eNamad / Samandehi footer widgets (sanitized HTML). Empty when none are shown.",
"items": {
"type": "object",
"properties": {
"kind": {
"type": "string",
"enum": ["enamad", "samandehi"]
},
"embedHtml": {
"type": "string",
"description": "Trusted HTML snippet (scripts stripped server-side)."
}
},
"required": ["kind", "embedHtml"]
}
}
}
}
}
}
}
}
}
},
"/tenants/{domain}/website/favicon": {
"get": {
"tags": [
"Homepage"
],
"summary": "Favicon and logo URLs for site chrome",
"description": "Use `faviconUrl` for `<link rel=\"icon\">` or Next.js `metadata.icons`. When no dedicated favicon is uploaded, `faviconUrl` equals `logoUrl`. Prefer this endpoint over tenant resolve when you only need branding assets.",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "Branding asset URLs",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"faviconUrl": {
"type": "string",
"nullable": true,
"description": "Use for browser tab icon. Falls back to logoUrl when no dedicated favicon exists."
},
"logoUrl": {
"type": "string",
"nullable": true,
"description": "Light-theme logo."
},
"logoDarkUrl": {
"type": "string",
"nullable": true,
"description": "Dark-theme logo; null when not set."
},
"hasDedicatedFavicon": {
"type": "boolean",
"description": "True when the business uploaded a separate favicon (not auto-derived from logo)."
}
},
"required": ["faviconUrl", "logoUrl", "logoDarkUrl", "hasDedicatedFavicon"]
}
}
}
},
"404": {
"description": "Unknown domain"
}
}
}
},
"/tenants/{domain}/analytics/views": {
"post": {
"tags": ["Analytics"],
"summary": "Record a storefront page view",
"description": "Optional explicit view counter. Most page views are recorded automatically on: (1) GET website/static-images?pageKey=home → home, (2) GET static-images?pageKey=about|contact|… → static_page, (3) public detail GETs for products, user-products, blogs, portfolios, videos, store-items. List APIs and homepage sliders do not count. Known bots / empty User-Agent are skipped. Refresh counts again. Use this POST for custom pages that do not hit those endpoints. No auth required. Events retained ~6 months; lifetime counters kept forever.",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": ["kind"],
"properties": {
"kind": {
"type": "string",
"enum": [
"home",
"static_page",
"portfolio_list",
"portfolio_detail",
"product_list",
"product_detail",
"store_item_list",
"store_item_detail",
"blog_list",
"blog_detail",
"video_list",
"video_detail",
"user_product_list",
"user_product_detail",
"website",
"product",
"portfolio",
"blog"
],
"description": "Canonical kinds preferred. Prefer home, static_page, and *_detail for page visits. Legacy aliases: website→home, product→product_detail, portfolio→portfolio_detail, blog→blog_detail."
},
"entityId": {
"type": "string",
"description": "Required for *_detail kinds (product, user product, portfolio, blog, video, store item)."
},
"path": {
"type": "string",
"description": "Optional page path (e.g. `/`, `/about`, `/blog/my-post`). Recommended for static_page."
}
}
}
}
}
},
"responses": {
"200": {
"description": "View recorded (or skipped for bots)",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"ok": { "type": "boolean", "enum": [true] },
"skipped": { "type": "string", "enum": ["bot"] }
}
}
}
}
},
"404": {
"description": "Unknown domain or entity"
}
}
}
},
"/tenants/{domain}/website/sliders": {
"get": {
"tags": [
"Homepage"
],
"summary": "Homepage sliders + slides",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "{ items: Slider[] }"
}
}
}
},
"/tenants/{domain}/website/static-images": {
"get": {
"tags": [
"Homepage"
],
"summary": "Named static image slots (single or list)",
"description": "Each slot has a stable `key` used in website placeholders and a `pageKey` (`home`, `products`, `about`, …). Filter with `?pageKey=home`. `kind` is `single` (one image) or `list` (ordered images). `itemCount` is the expected number of images when fixed (e.g. 2 for a two-banner row); omit/null means an unbounded list. Every image may include optional `titleFa`, `titleEn`, `subtext`, and `linkUrl`. Empty `images` means show the site fallback.",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "pageKey",
"in": "query",
"required": false,
"schema": {
"type": "string",
"example": "home"
},
"description": "If set, only slots for that website page are returned."
}
],
"responses": {
"200": {
"description": "{ items: StaticImageSlot[] } — key, pageKey, kind, label, aspectRatio, recommendedWidth, itemCount, images[{ id, url, titleFa, titleEn, subtext, linkUrl, width, height, sortOrder }]"
}
}
}
},
"/tenants/{domain}/website/static-images/{key}": {
"get": {
"tags": [
"Homepage"
],
"summary": "One static image slot by key",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "key",
"in": "path",
"required": true,
"schema": {
"type": "string",
"example": "home-hero"
}
}
],
"responses": {
"200": {
"description": "StaticImageSlot"
},
"404": {
"description": "Unknown slot key for this domain"
}
}
}
},
"/tenants/{domain}/website/category-groups": {
"get": {
"tags": [
"Homepage"
],
"summary": "Homepage category groups",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "{ items: CategoryGroup[] } — each group has id, key, title, items[]"
}
}
}
},
"/tenants/{domain}/website/brand-groups": {
"get": {
"tags": [
"Homepage"
],
"summary": "Homepage brand groups",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "{ items: BrandGroup[] } — each group has id, key, title, items[]"
}
}
}
},
"/tenants/{domain}/store-specials": {
"get": {
"tags": [
"Homepage",
"Store"
],
"summary": "Active store specials",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"responses": {
"200": {
"description": "{ source: product|store_item, items: StoreSpecial[] } — source is the business special-products setting; each special has id, key, title, items[]"
}
}
}
},
"/tenants/{domain}/categories": {
"get": {
"tags": [
"Categories"
],
"summary": "Public categories",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "entityType",
"in": "query",
"schema": {
"type": "string",
"enum": [
"product",
"blog",
"portfolio",
"video"
],
"default": "product"
}
}
],
"responses": {
"200": {
"description": "{ items: Category[] }"
}
}
}
},
"/tenants/{domain}/categories/by-id/{categoryId}": {
"get": {
"tags": [
"Categories"
],
"summary": "Category by id (preferred for /products/category/{id}/{nameFaSlug} pages)",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "categoryId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "entityType",
"in": "query",
"schema": {
"type": "string",
"enum": [
"product",
"blog",
"portfolio",
"video"
],
"default": "product"
}
}
],
"responses": {
"200": {
"description": "{ category } — use category.id as categoryId when listing products"
},
"404": {
"description": "Category not found"
}
}
}
},
"/tenants/{domain}/categories/by-slug/{slug}": {
"get": {
"tags": [
"Categories"
],
"summary": "Category by CMS slug (legacy / lookup helper)",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "slug",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "entityType",
"in": "query",
"schema": {
"type": "string",
"enum": [
"product",
"blog",
"portfolio",
"video"
],
"default": "product"
}
}
],
"responses": {
"200": {
"description": "{ category } — use category.id as categoryId when listing products"
},
"404": {
"description": "Category not found"
}
}
}
},
"/tenants/{domain}/products": {
"get": {
"tags": [
"Products"
],
"summary": "List published products",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "page",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"name": "pageSize",
"in": "query",
"schema": {
"type": "integer",
"default": 12
}
},
{
"name": "name",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "categoryId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "brandId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "tag",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "inStore",
"in": "query",
"schema": {
"type": "boolean"
}
}
],
"responses": {
"200": {
"description": "{ items, total, page, pageSize }"
}
}
}
},
"/tenants/{domain}/products/by-id/{productId}": {
"get": {
"tags": [
"Products"
],
"summary": "Product by id (preferred for /products/{id}/{nameFaSlug} pages)",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "productId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ product, relatedProducts }. `product.schema.jsonLd` is Product Schema.org JSON-LD when website structured data is enabled (null otherwise). Related products omit schema."
}
}
}
},
"/tenants/{domain}/products/by-id/{productId}/variations": {
"get": {
"tags": [
"Products"
],
"summary": "Product variations by id",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "productId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Variation tree for the product"
}
}
}
},
"/tenants/{domain}/products/by-id/{productId}/technical-info": {
"get": {
"tags": [
"Products"
],
"summary": "Product technical info by id",
"description": "Returns category technical form schema (field labels) plus this product’s submitted values. **Use this for the product specs / technical-details UI** — join `form.fields[].id` ↔ `values[].fieldId`. Product detail alone does not include field labels.",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "productId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ form: { id, categoryId, fields: [{ id, label, key, type, isRequired, sortOrder, options: [{ id, label, ... }] }] } | null, values: [{ fieldId, textValue?, optionId?, optionIds? }] }. Render label→value; hide section if form/fields/values empty."
}
}
}
},
"/tenants/{domain}/products/{slug}": {
"get": {
"tags": [
"Products"
],
"summary": "Product by slug",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "slug",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ product, relatedProducts }. `product.schema.jsonLd` is Product Schema.org JSON-LD when website structured data is enabled (null otherwise). Related products omit schema. relatedProducts uses the product list-card shape (incl. store). Ranked by relativity (same category, then same brand) with availability beside it (in-stock first within each group). Up to 8 items; excludes the current product."
}
}
}
},
"/tenants/{domain}/products/{slug}/variations": {
"get": {
"tags": [
"Products"
],
"summary": "Product variation options",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "slug",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ variations }"
}
}
}
},
"/tenants/{domain}/products/{slug}/technical-info": {
"get": {
"tags": [
"Products"
],
"summary": "Product technical specs (labels + values)",
"description": "Returns category technical form schema (field labels) plus this product’s submitted values. **Required for specs UI.** Join `form.fields[].id` ↔ `values[].fieldId`. Do not invent a separate category-fields public endpoint.",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "slug",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ form: { id, categoryId, fields: [{ id, label, key, type, isRequired, sortOrder, options }] } | null, values: [{ fieldId, textValue?, optionId?, optionIds? }] }"
}
}
}
},
"/tenants/{domain}/user-products": {
"get": {
"tags": [
"User Products"
],
"summary": "List published customer / stock listings",
"description": "Public marketplace-style listings created by customers (user products). Only `status=published`. Search with `name` or `q` (title/description). Filter by category, city, country, condition, or promoted.",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "page",
"in": "query",
"schema": {
"type": "integer",
"default": 1
}
},
{
"name": "pageSize",
"in": "query",
"schema": {
"type": "integer",
"default": 12
}
},
{
"name": "name",
"in": "query",
"description": "Search title/description (alias of q)",
"schema": {
"type": "string"
}
},
{
"name": "q",
"in": "query",
"description": "Search title/description (alias of name)",
"schema": {
"type": "string"
}
},
{
"name": "categoryId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "cityId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "countryId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "condition",
"in": "query",
"schema": {
"type": "string",
"enum": [
"new",
"stock",
"needs_repair",
"scrap"
]
}
},
{
"name": "promoted",
"in": "query",
"schema": {
"type": "boolean"
}
}
],
"responses": {
"200": {
"description": "{ items: UserProductListItem[], total, page, pageSize }. Each item includes id, slug, pathSlug, titleFa/titleEn, price, priceCurrency, condition, city/country names, imageUrl, category*, promoted, publishedAt."
}
}
}
},
"/tenants/{domain}/user-products/by-id/{productId}": {
"get": {
"tags": [
"User Products"
],
"summary": "User product by id (preferred for /user-products/{id}/{pathSlug} pages)",
"description": "Full published listing resolved by id. Prefer this for storefront detail pages — the path slug segment is SEO-only.\n\n**Important:** `technicalValues` items are **values only**. For a label→value table, call `GET /tenants/{domain}/user-products/by-id/{productId}/technical-info`.",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "productId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ product } with gallery, pathSlug, technicalValues (values only), countryId, cityId"
},
"404": {
"description": "Not found or not published"
}
}
}
},
"/tenants/{domain}/user-products/by-id/{productId}/technical-info": {
"get": {
"tags": [
"User Products"
],
"summary": "User product technical form + values by id",
"description": "**Use this for the listing specs UI** when the page is resolved by id. Same shape as the slug technical-info route.",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "productId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ form: { id, categoryId, fields: [...] } | null, values: [...] }"
}
}
}
},
"/tenants/{domain}/user-products/{slug}": {
"get": {
"tags": [
"User Products"
],
"summary": "User product details by slug",
"description": "Full published listing: location IDs, gallery images (`images`, `galleryMediaIds`), delivery/technical notes, and `technicalValues`.\n\n**Important:** `technicalValues` items are **values only** (`fieldId` + `textValue` / `optionId` / `optionIds`). They do **not** include field labels (`label`, `fieldName`, etc.). For a label→value technical-details table, call `GET /tenants/{domain}/user-products/{slug}/technical-info` and join on `fieldId`.",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "slug",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ product } with gallery (`images`: [{ mediaId, url }]), featuredMediaId, technicalValues (values only — no labels), countryId, cityId, countrySlug"
},
"404": {
"description": "Not found or not published"
}
}
}
},
"/tenants/{domain}/user-products/{slug}/technical-info": {
"get": {
"tags": [
"User Products"
],
"summary": "User product technical form + values (labels)",
"description": "**Use this for the listing specs / technical-details UI.** Returns the category technical form (field `id`, `label`, `key`, `type`, `sortOrder`, `options`) plus the listing’s submitted `values`.\n\nJoin `form.fields[].id` ↔ `values[].fieldId` and render label→value. Sort by `sortOrder`. Hide the section if `form`/fields/`values` are empty.\n\nThere is **no** public `product-category-variation-fields` (or similar) endpoint — use this path only.",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "slug",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ form: { id, categoryId, fields: [{ id, label, key, type, isRequired, sortOrder, options: [{ id, label, ... }] }] } | null, values: [{ fieldId, textValue?, optionId?, optionIds? }] }"
}
}
}
},
"/tenants/{domain}/store-items": {
"get": {
"tags": [
"Store"
],
"summary": "List sellable variants",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "page",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"name": "pageSize",
"in": "query",
"schema": {
"type": "integer",
"default": 20
}
},
{
"name": "categoryId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "brandId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "productId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "name",
"in": "query",
"description": "Instant search on product title / nameFa. Results are ordered in-stock first (unlimited or qty > 0), then by updatedAt desc.",
"schema": {
"type": "string"
}
},
{
"name": "inStock",
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "isFestival",
"in": "query",
"schema": {
"type": "boolean"
}
},
{
"name": "minPrice",
"in": "query",
"schema": {
"type": "number"
}
},
{
"name": "maxPrice",
"in": "query",
"schema": {
"type": "number"
}
}
],
"responses": {
"200": {
"description": "{ items, total, page, pageSize }"
}
}
}
},
"/tenants/{domain}/store-items/by-product/{productId}": {
"get": {
"tags": [
"Store"
],
"summary": "Variants for one product",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "productId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ storeItem }"
}
}
}
},
"/tenants/{domain}/store-items/{variantId}": {
"get": {
"tags": [
"Store"
],
"summary": "One variant",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "variantId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ variant }"
}
}
}
},
"/tenants/{domain}/torob_api/v3/products": {
"post": {
"tags": [
"Torob"
],
"summary": "Torob Product API v3 (store module only)",
"description": "Torob POSTs here (or to `https://{domain}/torob_api/v3/products` which nginx proxies). JWT in `X-Torob-Token` (EdDSA, aud = shop host). Returns 404 when the tenant does not have the **store** module. Page size is 100.",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "X-Torob-Token",
"in": "header",
"required": true,
"schema": {
"type": "string"
}
},
{
"name": "X-Torob-Token-Version",
"in": "header",
"schema": {
"type": "string",
"example": "1"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"oneOf": [
{
"type": "object",
"required": [
"page",
"sort"
],
"properties": {
"page": {
"type": "integer",
"minimum": 1
},
"sort": {
"type": "string",
"enum": [
"date_added_desc",
"date_updated_desc"
]
}
}
},
{
"type": "object",
"required": [
"page_urls"
],
"properties": {
"page_urls": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
}
}
},
{
"type": "object",
"required": [
"page_uniques"
],
"properties": {
"page_uniques": {
"type": "array",
"minItems": 1,
"items": {
"type": "string"
}
}
}
}
]
}
}
}
},
"responses": {
"200": {
"description": "{ api_version: torob_api_v3, current_page, total, max_pages, products[] }"
},
"400": {
"description": "{ error: string }"
},
"401": {
"description": "Invalid or missing Torob JWT"
},
"404": {
"description": "Unknown domain, store module off, or Torob switch off in store settings"
}
}
}
},
"/tenants/{domain}/blogs": {
"get": {
"tags": [
"Blogs"
],
"summary": "List published blogs",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "page",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"name": "pageSize",
"in": "query",
"schema": {
"type": "integer",
"default": 12
}
},
{
"name": "type",
"in": "query",
"schema": {
"type": "string",
"enum": [
"news",
"article",
"blog"
]
}
},
{
"name": "categoryId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "title",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "tag",
"in": "query",
"description": "Filter by exact tag in metadata.tags",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ items, total, page, pageSize }"
}
}
}
},
"/tenants/{domain}/blogs/by-id/{blogId}": {
"get": {
"tags": [
"Blogs"
],
"summary": "Blog by id (legacy; prefer slug routes `/blog/{slug}`)",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "blogId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ blog }. `blog.schema.jsonLd` is BlogPosting Schema.org JSON-LD when website structured data is enabled (null otherwise)."
}
}
}
},
"/tenants/{domain}/blogs/{slug}": {
"get": {
"tags": [
"Blogs"
],
"summary": "Blog by slug",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "slug",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ blog }. `blog.schema.jsonLd` is BlogPosting Schema.org JSON-LD when website structured data is enabled (null otherwise)."
}
}
}
},
"/tenants/{domain}/blogs/{blogId}/comments": {
"get": {
"tags": [
"Blogs",
"Comments"
],
"summary": "Approved blog comments",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "blogId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ items }"
}
}
},
"post": {
"tags": [
"Blogs",
"Comments"
],
"summary": "Submit blog comment",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "blogId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"authorName",
"text"
],
"properties": {
"authorName": {
"type": "string"
},
"authorEmail": {
"type": "string"
},
"text": {
"type": "string"
}
}
}
}
}
},
"responses": {
"201": {
"description": "{ comment, message }"
}
}
}
},
"/tenants/{domain}/videos": {
"get": {
"tags": [
"Videos"
],
"summary": "List published videos",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "page",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"name": "pageSize",
"in": "query",
"schema": {
"type": "integer",
"default": 12
}
},
{
"name": "provider",
"in": "query",
"schema": {
"type": "string",
"enum": [
"youtube",
"aparat"
]
}
},
{
"name": "categoryId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "title",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "tag",
"in": "query",
"description": "Filter by exact tag in metadata.tags",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ items, total, page, pageSize }"
}
}
}
},
"/tenants/{domain}/videos/{slug}": {
"get": {
"tags": [
"Videos"
],
"summary": "Video by slug",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "slug",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ video }"
}
}
}
},
"/tenants/{domain}/videos/{videoId}/comments": {
"get": {
"tags": [
"Videos",
"Comments"
],
"summary": "Approved video comments",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "videoId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ items }"
}
}
},
"post": {
"tags": [
"Videos",
"Comments"
],
"summary": "Submit video comment",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "videoId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"authorName",
"text"
],
"properties": {
"authorName": {
"type": "string"
},
"authorEmail": {
"type": "string"
},
"text": {
"type": "string"
}
}
}
}
}
},
"responses": {
"201": {
"description": "{ comment, message }"
}
}
}
},
"/tenants/{domain}/portfolios": {
"get": {
"tags": [
"Portfolios"
],
"summary": "List published portfolios",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "page",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"name": "pageSize",
"in": "query",
"schema": {
"type": "integer",
"default": 12
}
},
{
"name": "categoryId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "title",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "tag",
"in": "query",
"description": "Filter by exact tag in metadata.tags",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ items, total, page, pageSize }. Each item includes id, title/titleFa/titleEn, slug, abstract, projectUrl (nullable website link), status, categoryId/categoryName, tags, titleImageUrl, gallery, sortOrder, publishedAt, createdAt, updatedAt. Ordered by sortOrder desc, then publishedAt/createdAt desc."
}
}
}
},
"/tenants/{domain}/portfolios/by-id/{portfolioId}": {
"get": {
"tags": [
"Portfolios"
],
"summary": "Portfolio by id (legacy; prefer slug routes `/portfolio/{slug}`)",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "portfolioId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ portfolio } — same fields as list items, plus mainTextHtml, comments, and commentCount. Includes nullable projectUrl (external website link)."
}
}
}
},
"/tenants/{domain}/portfolios/{slug}": {
"get": {
"tags": [
"Portfolios"
],
"summary": "Portfolio by slug",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "slug",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ portfolio } — same fields as list items, plus mainTextHtml, comments, and commentCount. Includes nullable projectUrl (external website link)."
}
}
}
},
"/tenants/{domain}/portfolios/{portfolioId}/comments": {
"get": {
"tags": [
"Portfolios",
"Comments"
],
"summary": "Approved portfolio comments",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "portfolioId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ items }"
}
}
},
"post": {
"tags": [
"Portfolios",
"Comments"
],
"summary": "Submit portfolio comment",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "portfolioId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"authorName",
"text"
],
"properties": {
"authorName": {
"type": "string"
},
"authorEmail": {
"type": "string"
},
"text": {
"type": "string"
}
}
}
}
}
},
"responses": {
"201": {
"description": "{ comment, message }"
}
}
}
},
"/tenants/{domain}/comments": {
"get": {
"tags": [
"Comments"
],
"summary": "List approved comments for any entity",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "entityType",
"in": "query",
"required": true,
"schema": {
"type": "string",
"enum": [
"product",
"blog",
"portfolio",
"video"
]
}
},
{
"name": "entityId",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ items }"
}
}
},
"post": {
"tags": [
"Comments"
],
"summary": "Submit comment (product/blog/portfolio)",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"entityType",
"entityId",
"authorName",
"text"
],
"properties": {
"entityType": {
"type": "string",
"enum": [
"product",
"blog",
"portfolio",
"video"
]
},
"entityId": {
"type": "string"
},
"authorName": {
"type": "string"
},
"authorEmail": {
"type": "string"
},
"text": {
"type": "string"
}
}
}
}
}
},
"responses": {
"201": {
"description": "{ comment, message }"
}
}
}
},
"/tenants/{domain}/expert-reviews": {
"get": {
"tags": [
"Expert Reviews"
],
"summary": "Approved expert reviews for a product",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "productId",
"in": "query",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ items }"
}
}
},
"post": {
"tags": [
"Expert Reviews"
],
"summary": "Submit expert review",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"productId",
"authorName",
"rate",
"positivePoints",
"negativePoints",
"text"
],
"properties": {
"productId": {
"type": "string"
},
"authorName": {
"type": "string"
},
"authorEmail": {
"type": "string"
},
"rate": {
"type": "integer",
"minimum": 1,
"maximum": 10
},
"positivePoints": {
"type": "array",
"items": {
"type": "string"
}
},
"negativePoints": {
"type": "array",
"items": {
"type": "string"
}
},
"text": {
"type": "string"
}
}
}
}
}
},
"responses": {
"201": {
"description": "{ review, message }"
}
}
}
},
"/tenants/{domain}/contact-submissions": {
"post": {
"tags": [
"Contact"
],
"summary": "Contact form",
"parameters": [
{
"$ref": "#/components/parameters/domain"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"title",
"name",
"text"
],
"properties": {
"title": {
"type": "string"
},
"name": {
"type": "string"
},
"email": {
"type": "string"
},
"cellNumber": {
"type": "string"
},
"text": {
"type": "string"
}
}
}
}
}
},
"responses": {
"201": {
"description": "{ submission, message }"
}
}
}
},
"/auth/register": {
"post": {
"tags": [
"Auth"
],
"summary": "Register customer on a website",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"cellNumber",
"password",
"firstName",
"lastName",
"domain"
],
"properties": {
"cellNumber": {
"type": "string",
"description": "E.164 e.g. +98912..."
},
"password": {
"type": "string",
"minLength": 8
},
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"email": {
"type": "string"
},
"domain": {
"type": "string",
"description": "Same website apex as {domain}"
},
"acknowledgeExistingAccount": {
"type": "boolean",
"description": "If true, link an existing Meshkee account from another website without matching its password. Profile stays unchanged; password is replaced with the new signup password. Response may include passwordUpdated: true."
}
}
}
}
}
},
"responses": {
"201": {
"description": "{ user, accessToken, refreshToken, registeredBusiness }"
}
}
}
},
"/auth/login": {
"post": {
"tags": [
"Auth"
],
"summary": "Login",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"cellNumber",
"password"
],
"properties": {
"cellNumber": {
"type": "string"
},
"password": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "{ user, accessToken, refreshToken }"
}
}
}
},
"/auth/login-otp": {
"post": {
"tags": [
"Auth"
],
"summary": "Passwordless login with SMS OTP",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"cellNumber",
"code"
],
"properties": {
"cellNumber": {
"type": "string"
},
"code": {
"type": "string",
"minLength": 6,
"maxLength": 6
}
}
}
}
}
},
"responses": {
"200": {
"description": "{ user, accessToken, refreshToken }"
}
}
}
},
"/auth/reset-password": {
"post": {
"tags": [
"Auth"
],
"summary": "Reset password with SMS OTP",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"cellNumber",
"code",
"newPassword"
],
"properties": {
"cellNumber": {
"type": "string"
},
"code": {
"type": "string",
"minLength": 6,
"maxLength": 6
},
"newPassword": {
"type": "string",
"minLength": 8
}
}
}
}
}
},
"responses": {
"200": {
"description": "{ message }"
}
}
}
},
"/auth/refresh": {
"post": {
"tags": [
"Auth"
],
"summary": "Refresh tokens",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"refreshToken"
],
"properties": {
"refreshToken": {
"type": "string"
}
}
}
}
}
},
"responses": {
"200": {
"description": "{ user, accessToken, refreshToken }"
}
}
}
},
"/auth/me": {
"get": {
"tags": [
"Auth"
],
"summary": "Current user",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "{ user }"
}
}
}
},
"/auth/profile": {
"patch": {
"tags": [
"Auth"
],
"summary": "Update profile",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "{ message, user }"
}
}
}
},
"/auth/change-password": {
"post": {
"tags": [
"Auth"
],
"summary": "Change password",
"security": [
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"currentPassword",
"newPassword"
],
"properties": {
"currentPassword": {
"type": "string"
},
"newPassword": {
"type": "string",
"minLength": 8
}
}
}
}
}
},
"responses": {
"200": {
"description": "{ message }"
}
}
}
},
"/auth/send-otp": {
"post": {
"tags": [
"Auth"
],
"summary": "Send OTP SMS",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"cellNumber"
],
"properties": {
"cellNumber": {
"type": "string"
},
"domain": {
"type": "string",
"description": "Tenant host/apex used to brand the OTP SMS with the business Farsi name"
}
}
}
}
}
},
"responses": {
"200": {
"description": "{ enabled, message, expiresInSeconds? }"
}
}
}
},
"/auth/verify-otp": {
"post": {
"tags": [
"Auth"
],
"summary": "Verify OTP",
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"cellNumber",
"code"
],
"properties": {
"cellNumber": {
"type": "string"
},
"code": {
"type": "string",
"minLength": 6,
"maxLength": 6
}
}
}
}
}
},
"responses": {
"200": {
"description": "{ enabled, verified, message }"
}
}
}
},
"/auth/addresses": {
"get": {
"tags": [
"Addresses"
],
"summary": "List my shipping addresses",
"security": [
{
"bearerAuth": []
}
],
"responses": {
"200": {
"description": "{ items }"
}
}
},
"post": {
"tags": [
"Addresses"
],
"summary": "Create address",
"security": [
{
"bearerAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"province",
"city",
"address"
],
"properties": {
"label": {
"type": "string"
},
"province": {
"type": "string"
},
"city": {
"type": "string"
},
"address": {
"type": "string"
},
"postalCode": {
"type": "string"
},
"landline": {
"type": "string"
}
}
}
}
}
},
"responses": {
"201": {
"description": "{ address }"
}
}
}
},
"/auth/addresses/{addressId}": {
"patch": {
"tags": [
"Addresses"
],
"summary": "Update address",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"name": "addressId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ address }"
}
}
},
"delete": {
"tags": [
"Addresses"
],
"summary": "Delete address",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"name": "addressId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ message }"
}
}
}
},
"/cities": {
"get": {
"tags": [
"Cities"
],
"summary": "Location tree (countries / provinces / cities)",
"parameters": [
{
"name": "level",
"in": "query",
"schema": {
"type": "string",
"enum": [
"country",
"province",
"city"
]
}
},
{
"name": "parentId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "parentSlug",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ items }"
}
}
}
},
"/cities/{cityId}": {
"get": {
"tags": [
"Cities"
],
"summary": "Get one location node",
"parameters": [
{
"name": "cityId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ city }"
}
}
}
},
"/businesses/{businessId}/cart": {
"get": {
"tags": [
"Cart"
],
"summary": "Get cart",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/businessId"
}
],
"responses": {
"200": {
"description": "{ cart }"
}
}
},
"delete": {
"tags": [
"Cart"
],
"summary": "Clear cart",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/businessId"
}
],
"responses": {
"200": {
"description": "{ message, cart }"
}
}
}
},
"/businesses/{businessId}/cart/items": {
"post": {
"tags": [
"Cart"
],
"summary": "Add variant to cart",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/businessId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"storeItemVariantId"
],
"properties": {
"storeItemVariantId": {
"type": "string"
},
"quantity": {
"type": "integer",
"minimum": 1,
"default": 1
}
}
}
}
}
},
"responses": {
"201": {
"description": "{ message, cart }"
}
}
}
},
"/businesses/{businessId}/cart/items/{itemId}": {
"patch": {
"tags": [
"Cart"
],
"summary": "Update cart line quantity",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/businessId"
},
{
"name": "itemId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"quantity"
],
"properties": {
"quantity": {
"type": "integer",
"minimum": 1
}
}
}
}
}
},
"responses": {
"200": {
"description": "{ message, cart }"
}
}
},
"delete": {
"tags": [
"Cart"
],
"summary": "Remove cart line",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/businessId"
},
{
"name": "itemId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ message, cart }"
}
}
}
},
"/businesses/{businessId}/cart/checkout": {
"post": {
"tags": [
"Cart"
],
"summary": "Checkout → create order",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/businessId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"payment"
],
"properties": {
"addressId": {
"type": "string"
},
"shippingAddress": {
"type": "object",
"properties": {
"province": {
"type": "string"
},
"city": {
"type": "string"
},
"address": {
"type": "string"
},
"postalCode": {
"type": "string"
},
"landline": {
"type": "string"
}
}
},
"customerNotes": {
"type": "string"
},
"payment": {
"type": "object",
"required": [
"type"
],
"properties": {
"type": {
"type": "string",
"enum": [
"pos",
"cash",
"transfer",
"e_payment_gate"
]
},
"posType": {
"type": "string"
},
"transferAccount": {
"type": "string"
},
"transferRefNumber": {
"type": "string"
},
"gatewayType": {
"type": "string",
"enum": [
"mellat",
"sep",
"snappay",
"digipay",
"zarinpal"
],
"description": "Required when type is e_payment_gate (defaults to store defaultGateway if omitted)."
},
"notes": {
"type": "string"
}
}
},
"returnUrl": {
"type": "string",
"format": "uri",
"description": "Required for e_payment_gate. Absolute URL the bank callback redirects the shopper to (status, orderId, transactionId query params appended)."
}
}
}
}
}
},
"responses": {
"201": {
"description": "Cash/transfer/pos: { message, order }. E-payment: { message, order, payment: { gatewayType, transactionId, redirect: { method, url, fields } } }"
}
}
}
},
"/businesses/{businessId}/orders": {
"get": {
"tags": [
"Orders"
],
"summary": "My orders",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/businessId"
},
{
"name": "page",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"name": "pageSize",
"in": "query",
"schema": {
"type": "integer",
"default": 20
}
},
{
"name": "status",
"in": "query",
"schema": {
"type": "string",
"enum": [
"pending",
"confirmed",
"processing",
"shipped",
"delivered",
"cancelled"
]
}
}
],
"responses": {
"200": {
"description": "{ items, total, page, pageSize }"
}
}
}
},
"/businesses/{businessId}/orders/{orderId}": {
"get": {
"tags": [
"Orders"
],
"summary": "My order detail",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/businessId"
},
{
"name": "orderId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ order }"
}
}
}
},
"/businesses/{businessId}/favorites": {
"get": {
"tags": [
"Favorites"
],
"summary": "List favorites",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/businessId"
},
{
"name": "page",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"name": "pageSize",
"in": "query",
"schema": {
"type": "integer",
"default": 20
}
}
],
"responses": {
"200": {
"description": "{ items, total, page, pageSize }"
}
}
},
"post": {
"tags": [
"Favorites"
],
"summary": "Add favorite",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/businessId"
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"productId"
],
"properties": {
"productId": {
"type": "string"
}
}
}
}
}
},
"responses": {
"201": {
"description": "{ favorite, message }"
}
}
}
},
"/businesses/{businessId}/favorites/{productId}": {
"delete": {
"tags": [
"Favorites"
],
"summary": "Remove favorite",
"security": [
{
"bearerAuth": []
}
],
"parameters": [
{
"$ref": "#/components/parameters/businessId"
},
{
"name": "productId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ message }"
}
}
}
},
"/public/sms/send": {
"post": {
"tags": [
"Partner SMS"
],
"summary": "Send SMS via Meshkee (partner gateway)",
"description": "Server-to-server only. For external/partner backends (e.g. Balout) that need to send SMS through Meshkee → Gama. Not for browser/storefront JS. Requires an allowlisted `domain` + matching `X-Api-Key`. See /docs/website/SMS.md.",
"security": [
{
"apiKeyAuth": []
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"domain",
"to",
"message"
],
"properties": {
"domain": {
"type": "string",
"example": "baloutpastry.com",
"description": "Allowlisted partner apex (www. is stripped)"
},
"to": {
"type": "string",
"example": "09127004945",
"description": "Iranian mobile: 09…, 9…, +989…, or 989…"
},
"message": {
"type": "string",
"maxLength": 700,
"example": "سفارش شما به شماره ی ۱۲۱۱۳۲۲ اماده می باشد."
}
}
}
}
}
},
"responses": {
"200": {
"description": "Accepted by Gama",
"content": {
"application/json": {
"schema": {
"type": "object",
"properties": {
"success": {
"type": "boolean",
"example": true
},
"serverId": {
"type": "string",
"example": "1136923081051406337"
}
}
}
}
}
},
"400": {
"description": "Invalid phone or message"
},
"401": {
"description": "Missing/invalid X-Api-Key or domain"
},
"429": {
"description": "Rate limited (30/partner/min or 5/destination/min)"
},
"503": {
"description": "SMS disabled or provider unreachable"
}
}
}
},
"/tenants/{domain}/instructions": {
"get": {
"tags": [
"Instructions"
],
"summary": "List published instructions",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "page",
"in": "query",
"schema": {
"type": "integer"
}
},
{
"name": "pageSize",
"in": "query",
"schema": {
"type": "integer",
"default": 12
}
},
{
"name": "provider",
"in": "query",
"schema": {
"type": "string",
"enum": [
"youtube",
"aparat"
]
}
},
{
"name": "categoryId",
"in": "query",
"schema": {
"type": "string"
}
},
{
"name": "title",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ items, total, page, pageSize }"
}
}
}
},
"/tenants/{domain}/instructions/{slug}": {
"get": {
"tags": [
"Instructions"
],
"summary": "Instruction by slug",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "slug",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ instruction }"
}
}
}
},
"/tenants/{domain}/instructions/{instructionId}/comments": {
"get": {
"tags": [
"Instructions",
"Comments"
],
"summary": "Approved instruction comments",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "instructionId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "{ items }"
}
}
},
"post": {
"tags": [
"Instructions",
"Comments"
],
"summary": "Submit instruction comment",
"parameters": [
{
"$ref": "#/components/parameters/domain"
},
{
"name": "instructionId",
"in": "path",
"required": true,
"schema": {
"type": "string"
}
}
],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"type": "object",
"required": [
"authorName",
"text"
],
"properties": {
"authorName": {
"type": "string"
},
"authorEmail": {
"type": "string"
},
"text": {
"type": "string"
}
}
}
}
}
},
"responses": {
"201": {
"description": "{ comment, message }"
}
}
}
},
"/tenants/{domain}/workshops": {
"get": {
"tags": ["Workshops"],
"summary": "List published workshops",
"parameters": [
{ "$ref": "#/components/parameters/domain" },
{ "name": "page", "in": "query", "schema": { "type": "integer" } },
{ "name": "pageSize", "in": "query", "schema": { "type": "integer", "default": 12 } },
{ "name": "categoryId", "in": "query", "schema": { "type": "string" } },
{ "name": "title", "in": "query", "schema": { "type": "string" } }
],
"responses": {
"200": { "description": "{ items, total, page, pageSize }" }
}
}
},
"/tenants/{domain}/workshops/{slug}": {
"get": {
"tags": ["Workshops"],
"summary": "Workshop by slug",
"parameters": [
{ "$ref": "#/components/parameters/domain" },
{ "name": "slug", "in": "path", "required": true, "schema": { "type": "string" } }
],
"responses": {
"200": { "description": "{ workshop } — includes eventDate, eventTime, duration" }
}
}
},
"/tenants/{domain}/workshops/{workshopId}/register": {
"post": {
"tags": ["Workshops"],
"summary": "Register logged-in website customer for a workshop",
"security": [{ "bearerAuth": [] }],
"parameters": [
{ "$ref": "#/components/parameters/domain" },
{ "name": "workshopId", "in": "path", "required": true, "schema": { "type": "string" } }
],
"responses": {
"201": { "description": "{ registration: { id, status: pending|approved|rejected, ... }, message }" },
"409": { "description": "Already registered" }
}
},
"delete": {
"tags": ["Workshops"],
"summary": "Cancel workshop registration",
"security": [{ "bearerAuth": [] }],
"parameters": [
{ "$ref": "#/components/parameters/domain" },
{ "name": "workshopId", "in": "path", "required": true, "schema": { "type": "string" } }
],
"responses": {
"200": { "description": "{ message }" }
}
}
},
"/businesses/{businessId}/payments/methods": {
"get": {
"tags": [
"Payments"
],
"summary": "List enabled online payment gateways (public)",
"parameters": [
{
"$ref": "#/components/parameters/businessId"
}
],
"responses": {
"200": {
"description": "{ enabled, defaultGateway, gateways: [{ id, label, labelFa }] } — no secrets"
}
}
}
},
"/businesses/{businessId}/payments/{gateway}/callback": {
"post": {
"tags": [
"Payments"
],
"summary": "Bank payment callback — legacy direct API URL (prefer store-domain path)",
"description": "Still supported. Prefer https://{store}/meshkee/payments/{gateway}/callback so gateway domain checks match the merchant terminal.",
"parameters": [
{
"$ref": "#/components/parameters/businessId"
},
{
"name": "gateway",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"mellat",
"sep",
"snappay",
"digipay",
"zarinpal"
]
}
}
],
"requestBody": {
"required": true,
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
},
"description": "Gateway-specific fields (Mellat: ResCode, SaleOrderId, SaleReferenceId, RefId, …)"
}
}
}
},
"responses": {
"303": {
"description": "Redirect to checkout returnUrl with status=success|failed"
}
}
},
"get": {
"tags": [
"Payments"
],
"summary": "Bank payment callback GET — legacy direct API URL",
"parameters": [
{
"$ref": "#/components/parameters/businessId"
},
{
"name": "gateway",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"mellat",
"sep",
"snappay",
"digipay",
"zarinpal"
]
}
}
],
"responses": {
"303": {
"description": "Redirect to checkout returnUrl with status=success|failed"
}
}
}
},
"/tenants/{host}/payments/{gateway}/callback": {
"post": {
"tags": [
"Payments"
],
"summary": "Bank payment callback via store host (nginx proxies /meshkee/payments/... here)",
"description": "Public. Resolves business from host. Used after storefront nginx proxies https://{store}/meshkee/payments/{gateway}/callback.",
"parameters": [
{
"name": "host",
"in": "path",
"required": true,
"schema": {
"type": "string",
"example": "YOUR_WEBSITE_DOMAIN"
}
},
{
"name": "gateway",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"mellat",
"sep",
"snappay",
"digipay",
"zarinpal"
]
}
}
],
"requestBody": {
"required": true,
"content": {
"application/x-www-form-urlencoded": {
"schema": {
"type": "object",
"additionalProperties": {
"type": "string"
}
}
}
}
},
"responses": {
"303": {
"description": "Redirect to checkout returnUrl with status=success|failed"
}
}
},
"get": {
"tags": [
"Payments"
],
"summary": "Bank payment callback GET via store host (ZarinPal)",
"parameters": [
{
"name": "host",
"in": "path",
"required": true,
"schema": {
"type": "string",
"example": "YOUR_WEBSITE_DOMAIN"
}
},
{
"name": "gateway",
"in": "path",
"required": true,
"schema": {
"type": "string",
"enum": [
"mellat",
"sep",
"snappay",
"digipay",
"zarinpal"
]
}
}
],
"responses": {
"303": {
"description": "Redirect to checkout returnUrl with status=success|failed"
}
}
}
}
}
}