{
  "openapi": "3.1.0",
  "info": {
    "title": "Movement OMS Public API",
    "version": "1.1.0",
    "description": "Org-scoped API for dealer storefronts and integrations. **Never a marketplace** — every response is limited to the Organization on your API key.\n\nAuthenticate with `Authorization: Bearer mov_live_…`. Create and revoke keys in the OMS app under Settings.\n\nRennwrist.com is the first customer of this API (plan `free_forever`)."
  },
  "servers": [
    {
      "url": "https://rennwrist.com",
      "description": "Current OMS host (until app.movementoms.com cutover)"
    },
    {
      "url": "https://app.movementoms.com",
      "description": "Movement OMS app host (target)"
    },
    {
      "url": "http://localhost:3000",
      "description": "Local OMS"
    }
  ],
  "tags": [
    { "name": "Inventory", "description": "Public catalog for your organization only" },
    { "name": "Journal", "description": "Published articles (entitlement: journalCms)" },
    { "name": "Leads", "description": "Inbound leads into your CRM (inquire, sell, source, access, notify, waitlist)" },
    { "name": "Subscribers", "description": "Newsletter capture with double opt-in (entitlement: newsletters). Requires name + email. POST again to resend after cooldown." },
    { "name": "Checkout", "description": "Stripe Checkout sessions (entitlement: commerceCheckout)" }
  ],
  "components": {
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "mov_live_",
        "description": "Organization API key. Org is inferred from the key — never send an organization id."
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "properties": {
          "error": { "type": "string" },
          "code": { "type": "string" }
        }
      },
      "OrganizationRef": {
        "type": "object",
        "properties": {
          "slug": { "type": "string", "example": "rennwrist" },
          "name": { "type": "string", "example": "Rennwrist" }
        }
      },
      "InventoryItem": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "slug": { "type": "string", "nullable": true },
          "brand": { "type": "string", "nullable": true },
          "model": { "type": "string", "nullable": true },
          "reference": { "type": "string", "nullable": true },
          "stockIdentifier": { "type": "string", "nullable": true },
          "status": { "type": "string", "description": "available | pending | sold | …" },
          "assetType": { "type": "string", "enum": ["watch", "car", "art"] },
          "condition": { "type": "string", "nullable": true },
          "priceCents": { "type": "integer", "nullable": true },
          "priceOrInquire": { "type": "string", "nullable": true },
          "description": { "type": "string", "nullable": true },
          "seoTitle": { "type": "string", "nullable": true },
          "seoDescription": { "type": "string", "nullable": true },
          "soldAt": { "type": "string", "format": "date-time", "nullable": true },
          "primaryImageUrl": { "type": "string", "nullable": true },
          "href": { "type": "string", "description": "Storefront path, e.g. /watches/{id}/{slug}" },
          "images": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "url": { "type": "string" },
                "caption": { "type": "string", "nullable": true }
              }
            }
          },
          "related": {
            "type": "array",
            "items": { "$ref": "#/components/schemas/InventoryItem" }
          }
        }
      },
      "InventoryCounts": {
        "type": "object",
        "description": "Availability only — never sold totals. Use counts.available for storefront AVAILABLE readouts.",
        "properties": {
          "available": {
            "type": "integer",
            "description": "Count of status=available matching current filters (status query ignored for this count)"
          },
          "byAssetType": {
            "type": "object",
            "properties": {
              "watch": { "type": "integer" },
              "car": { "type": "integer" },
              "art": { "type": "integer" }
            }
          }
        }
      },
      "JournalPostSummary": {
        "type": "object",
        "properties": {
          "id": { "type": "integer" },
          "title": { "type": "string" },
          "slug": { "type": "string" },
          "excerpt": { "type": "string", "nullable": true },
          "imageUrl": { "type": "string", "nullable": true },
          "imageAlt": { "type": "string", "nullable": true },
          "publishedAt": { "type": "string", "format": "date-time" }
        }
      },
      "LeadCreate": {
        "type": "object",
        "required": ["name", "email", "phone"],
        "properties": {
          "name": { "type": "string" },
          "email": { "type": "string", "format": "email" },
          "phone": { "type": "string" },
          "message": { "type": "string" },
          "type": {
            "type": "string",
            "enum": [
              "inquire",
              "sell",
              "source",
              "reservation",
              "consign",
              "request_access",
              "notify",
              "waitlist",
              "queue",
              "request_photos"
            ],
            "default": "inquire",
            "description": "notify = sold-listing alert; waitlist/queue = pending second buyer; request_photos = gallery failure CTA. inventoryItemId required for notify, waitlist, queue, request_photos."
          },
          "inventoryItemId": {
            "type": "integer",
            "description": "Optional public inventory item in this org; required for notify / waitlist / queue / request_photos"
          },
          "photoRefs": {
            "type": "array",
            "items": { "type": "string" },
            "description": "File refs from POST /api/v1/leads/attachments"
          },
          "meta": {
            "type": "object",
            "additionalProperties": true,
            "description": "Free-form form fields (brands, budget, condition, tradeInterest, …)"
          },
          "utm": { "type": "object", "additionalProperties": true }
        }
      },
      "CheckoutCreate": {
        "type": "object",
        "required": ["inventoryItemId", "name", "email", "phone"],
        "properties": {
          "inventoryItemId": { "type": "integer" },
          "name": { "type": "string" },
          "email": { "type": "string", "format": "email" },
          "phone": { "type": "string" },
          "message": { "type": "string" },
          "successUrl": {
            "type": "string",
            "description": "Must include the literal placeholder {CHECKOUT_SESSION_ID}"
          },
          "cancelUrl": { "type": "string" }
        }
      },
      "SubscriberCreate": {
        "type": "object",
        "required": ["name", "email"],
        "properties": {
          "name": { "type": "string" },
          "email": { "type": "string", "format": "email" },
          "source": {
            "type": "string",
            "description": "Optional free-form source label (footer, journal, Zapier, …)"
          }
        }
      }
    }
  },
  "security": [{ "bearerAuth": [] }],
  "paths": {
    "/api/v1/inventory": {
      "get": {
        "tags": ["Inventory"],
        "summary": "List public inventory",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": { "type": "integer", "default": 1 }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "default": 24, "maximum": 100 }
          },
          {
            "name": "sort",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": ["price_desc", "price_asc", "newest", "oldest", "updated", "brand"],
              "default": "updated"
            }
          },
          {
            "name": "assetType",
            "in": "query",
            "schema": { "type": "string", "enum": ["watch", "car", "art"] }
          },
          {
            "name": "brand",
            "in": "query",
            "schema": { "type": "string" }
          },
          {
            "name": "model",
            "in": "query",
            "schema": { "type": "string" }
          },
          {
            "name": "reference",
            "in": "query",
            "schema": { "type": "string" }
          },
          {
            "name": "status",
            "in": "query",
            "description": "Filter list rows (e.g. available for hide-sold). Does not change counts.available.",
            "schema": { "type": "string" }
          },
          {
            "name": "q",
            "in": "query",
            "description": "Search brand, model, reference, stock id, slug",
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Paged inventory for the API key's organization only",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "organization": { "$ref": "#/components/schemas/OrganizationRef" },
                    "page": { "type": "integer" },
                    "limit": { "type": "integer" },
                    "total": {
                      "type": "integer",
                      "description": "Total matching rows (including sold). Prefer counts.available for storefront chrome; do not publish sold or total-listing figures to collectors."
                    },
                    "hasMore": { "type": "boolean" },
                    "sort": { "type": "string" },
                    "counts": { "$ref": "#/components/schemas/InventoryCounts" },
                    "filters": {
                      "type": "object",
                      "properties": {
                        "brands": {
                          "type": "array",
                          "items": { "type": "string" }
                        }
                      }
                    },
                    "items": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/InventoryItem" }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing or invalid API key",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/v1/inventory/{idOrSlug}": {
      "get": {
        "tags": ["Inventory"],
        "summary": "Get one public inventory item",
        "parameters": [
          {
            "name": "idOrSlug",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": {
            "description": "Item detail",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "organization": { "$ref": "#/components/schemas/OrganizationRef" },
                    "item": { "$ref": "#/components/schemas/InventoryItem" }
                  }
                }
              }
            }
          },
          "404": {
            "description": "Not found in this organization",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/Error" }
              }
            }
          }
        }
      }
    },
    "/api/v1/journal": {
      "get": {
        "tags": ["Journal"],
        "summary": "List published journal posts",
        "parameters": [
          {
            "name": "page",
            "in": "query",
            "schema": { "type": "integer", "default": 1 }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": { "type": "integer", "default": 20, "maximum": 50 }
          }
        ],
        "responses": {
          "200": {
            "description": "Paged journal",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "organization": { "$ref": "#/components/schemas/OrganizationRef" },
                    "page": { "type": "integer" },
                    "limit": { "type": "integer" },
                    "total": { "type": "integer" },
                    "items": {
                      "type": "array",
                      "items": { "$ref": "#/components/schemas/JournalPostSummary" }
                    }
                  }
                }
              }
            }
          },
          "403": {
            "description": "journalCms entitlement required"
          }
        }
      }
    },
    "/api/v1/journal/{slug}": {
      "get": {
        "tags": ["Journal"],
        "summary": "Get one published journal post",
        "parameters": [
          {
            "name": "slug",
            "in": "path",
            "required": true,
            "schema": { "type": "string" }
          }
        ],
        "responses": {
          "200": { "description": "Post with HTML body" },
          "403": { "description": "journalCms entitlement required" },
          "404": { "description": "Not found" }
        }
      }
    },
    "/api/v1/leads": {
      "post": {
        "tags": ["Leads"],
        "summary": "Create a lead",
        "description": "Creates a CRM Submission (or Application for request_access). request_access writes an Application; other types write a Submission.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/LeadCreate" }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Lead created in the org CRM",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "organization": { "$ref": "#/components/schemas/OrganizationRef" },
                    "id": { "type": "integer" },
                    "type": { "type": "string", "example": "inquire" },
                    "customerId": { "type": "integer" },
                    "inventoryItemId": { "type": "integer", "nullable": true },
                    "applicationId": { "type": "integer", "description": "Present when type=request_access" }
                  }
                }
              }
            }
          },
          "400": { "description": "Validation error" }
        }
      }
    },
    "/api/v1/leads/attachments": {
      "post": {
        "tags": ["Leads"],
        "summary": "Upload lead photos",
        "description": "Multipart upload (`photos` or `photo` fields). Returns fileRefs to include on POST /api/v1/leads. Dev writes local disk; production uses GCS when configured.",
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "photos": {
                    "type": "array",
                    "items": { "type": "string", "format": "binary" }
                  },
                  "photo": { "type": "string", "format": "binary" }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Uploaded refs",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "organization": { "$ref": "#/components/schemas/OrganizationRef" },
                    "fileRefs": {
                      "type": "array",
                      "items": { "type": "string" }
                    }
                  }
                }
              }
            }
          },
          "400": { "description": "Validation error" },
          "401": { "description": "Unauthorized" }
        }
      }
    },
    "/api/v1/subscribers": {
      "post": {
        "tags": ["Subscribers"],
        "summary": "Start newsletter double opt-in",
        "description": "Creates or finds a Customer, sets newsletter pending, and emails a confirmation link. Marketing opt-in flips only after they confirm at `/subscribe/confirm?token=…`. Name and email required. Server-side only (Bearer key).\n\n**Resend:** POST the same payload again after the cooldown (~15 minutes). While on cooldown, response is still `pending_confirmation` with `retryAfterSeconds`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SubscriberCreate" }
            }
          }
        },
        "responses": {
          "202": {
            "description": "Confirmation email sent (or cooldown active)",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "organization": { "$ref": "#/components/schemas/OrganizationRef" },
                    "status": {
                      "type": "string",
                      "enum": ["pending_confirmation"]
                    },
                    "customerId": { "type": "integer" },
                    "retryAfterSeconds": { "type": "integer" }
                  }
                }
              }
            }
          },
          "200": {
            "description": "Already subscribed",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": { "type": "string", "enum": ["already_subscribed"] },
                    "customerId": { "type": "integer" }
                  }
                }
              }
            }
          },
          "400": { "description": "Validation error" },
          "403": { "description": "newsletters entitlement required" }
        }
      }
    },
    "/api/v1/checkout/sessions": {
      "post": {
        "tags": ["Checkout"],
        "summary": "Create a Stripe Checkout session",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/CheckoutCreate" }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Checkout URL",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "organization": { "$ref": "#/components/schemas/OrganizationRef" },
                    "url": { "type": "string", "format": "uri" },
                    "dealId": { "type": "integer" },
                    "inventoryItemId": { "type": "integer" }
                  }
                }
              }
            }
          },
          "403": { "description": "commerceCheckout entitlement required" }
        }
      }
    }
  }
}
