openapi: 3.0.3 info: title: Dutchie Analytics API description: | Comprehensive REST API for Arizona dispensary data aggregation and analytics. ## Features - Product data with high-resolution images (2000x2000) - Brand tracking and market share analysis - Daily specials grouped by date - Price history and trend analysis - Inventory tracking (in stock/out of stock) - Real-time scraping with proxy management ## Authentication All endpoints require JWT authentication via Bearer token. ## Rate Limiting - Standard users: 100 requests/minute - Admin users: 500 requests/minute ## Image Storage All images are stored in MinIO (S3-compatible) and served via CDN. version: 1.0.0 contact: name: API Support email: support@dutchieanalytics.com license: name: Proprietary servers: - url: http://localhost:3010/api description: Development server - url: https://api.dutchieanalytics.com/api description: Production server components: securitySchemes: bearerAuth: type: http scheme: bearer bearerFormat: JWT schemas: Store: type: object properties: id: type: integer example: 1 name: type: string example: "Curaleaf - Phoenix" slug: type: string example: "curaleaf-az-phoenix" dutchie_url: type: string format: uri example: "https://dutchie.com/dispensary/curaleaf-phoenix" logo_url: type: string format: uri nullable: true active: type: boolean example: true scrape_enabled: type: boolean example: true product_count: type: integer example: 345 category_count: type: integer example: 12 last_scraped_at: type: string format: date-time nullable: true created_at: type: string format: date-time updated_at: type: string format: date-time Product: type: object properties: id: type: integer example: 1 dutchie_product_id: type: string example: "12345" store_id: type: integer example: 1 category_id: type: integer nullable: true example: 5 name: type: string example: "Blue Dream 3.5g" brand: type: string nullable: true example: "Cresco Labs" description: type: string nullable: true price: type: number format: float nullable: true example: 45.00 weight: type: string nullable: true example: "3.5g" thc_percentage: type: number format: float nullable: true example: 24.5 cbd_percentage: type: number format: float nullable: true example: 0.5 strain_type: type: string nullable: true enum: [indica, sativa, hybrid] example: "hybrid" in_stock: type: boolean example: true image_url: type: string format: uri nullable: true description: "Original Dutchie image URL" image_url_full: type: string format: uri nullable: true description: "High-resolution image from MinIO (2000x2000)" local_image_path: type: string nullable: true description: "MinIO storage path" thumbnail_path: type: string nullable: true description: "Thumbnail image path (300x300)" medium_path: type: string nullable: true description: "Medium image path (800x800)" full_path: type: string nullable: true description: "Full resolution image path (2000x2000)" dutchie_url: type: string format: uri nullable: true store_name: type: string example: "Curaleaf - Phoenix" category_name: type: string nullable: true example: "Flower" last_seen_at: type: string format: date-time created_at: type: string format: date-time Category: type: object properties: id: type: integer example: 1 store_id: type: integer example: 1 parent_id: type: integer nullable: true example: null name: type: string example: "Flower" slug: type: string example: "flower" dutchie_url: type: string format: uri nullable: true display_order: type: integer example: 1 product_count: type: integer example: 145 parent_name: type: string nullable: true created_at: type: string format: date-time Brand: type: object properties: name: type: string example: "Cresco Labs" Special: type: object properties: id: type: integer store_id: type: integer product_id: type: integer nullable: true name: type: string example: "$5 OFF Blue Dream" description: type: string nullable: true discount_amount: type: number format: float nullable: true discount_percentage: type: number format: float nullable: true valid_date: type: string format: date product_name: type: string nullable: true product_image: type: string format: uri nullable: true Error: type: object properties: error: type: string example: "Resource not found" security: - bearerAuth: [] paths: /auth/login: post: tags: - Authentication summary: Login description: Authenticate user and receive JWT token security: [] requestBody: required: true content: application/json: schema: type: object required: - email - password properties: email: type: string format: email example: "admin@example.com" password: type: string format: password example: "password123" responses: '200': description: Login successful content: application/json: schema: type: object properties: token: type: string user: type: object properties: id: type: integer email: type: string role: type: string enum: [superadmin, admin, user] '401': description: Invalid credentials content: application/json: schema: $ref: '#/components/schemas/Error' /stores: get: tags: - Stores summary: Get all stores description: Retrieve list of all dispensary stores with product and category counts responses: '200': description: Successful response content: application/json: schema: type: object properties: stores: type: array items: $ref: '#/components/schemas/Store' post: tags: - Stores summary: Create store description: Add a new dispensary store (admin only) requestBody: required: true content: application/json: schema: type: object required: - name - slug - dutchie_url properties: name: type: string slug: type: string dutchie_url: type: string format: uri active: type: boolean default: true scrape_enabled: type: boolean default: true responses: '201': description: Store created content: application/json: schema: $ref: '#/components/schemas/Store' '401': description: Unauthorized /stores/{id}: get: tags: - Stores summary: Get single store description: Retrieve detailed information about a specific store parameters: - name: id in: path required: true schema: type: integer example: 1 responses: '200': description: Successful response content: application/json: schema: $ref: '#/components/schemas/Store' '404': description: Store not found put: tags: - Stores summary: Update store description: Update store information (admin only) parameters: - name: id in: path required: true schema: type: integer requestBody: content: application/json: schema: type: object properties: name: type: string slug: type: string dutchie_url: type: string active: type: boolean scrape_enabled: type: boolean responses: '200': description: Store updated content: application/json: schema: $ref: '#/components/schemas/Store' delete: tags: - Stores summary: Delete store description: Delete a store (superadmin only) parameters: - name: id in: path required: true schema: type: integer responses: '200': description: Store deleted /stores/{id}/brands: get: tags: - Stores summary: Get store brands description: Get list of all brands available at a store parameters: - name: id in: path required: true schema: type: integer responses: '200': description: Successful response content: application/json: schema: type: object properties: brands: type: array items: type: string example: ["Cresco Labs", "Select", "Timeless"] /stores/{id}/specials: get: tags: - Stores summary: Get store specials description: Get daily specials for a store on a specific date parameters: - name: id in: path required: true schema: type: integer - name: date in: query required: false schema: type: string format: date description: Date for specials (defaults to today) example: "2025-01-15" responses: '200': description: Successful response content: application/json: schema: type: object properties: specials: type: array items: $ref: '#/components/schemas/Special' date: type: string format: date /stores/{id}/scrape: post: tags: - Stores summary: Trigger store scrape description: Start scraping process for a store (admin only) parameters: - name: id in: path required: true schema: type: integer requestBody: content: application/json: schema: type: object properties: parallel: type: integer default: 3 description: Number of parallel scrapers userAgent: type: string description: Custom user agent (optional) responses: '200': description: Scrape started content: application/json: schema: type: object properties: message: type: string parallel: type: integer userAgent: type: string /stores/{id}/discover-categories: post: tags: - Stores summary: Discover categories description: Discover and import categories from Dutchie menu (admin only) parameters: - name: id in: path required: true schema: type: integer responses: '200': description: Category discovery started /products: get: tags: - Products summary: Get products description: | Retrieve products with filtering, searching, and pagination. ## Field Selection Use the `fields` parameter to select specific fields: - `fields=id,name,price` - Only return specified fields - Reduces payload size and improves performance parameters: - name: store_id in: query schema: type: integer description: Filter by store ID - name: category_id in: query schema: type: integer description: Filter by category ID - name: in_stock in: query schema: type: boolean description: Filter by stock availability - name: search in: query schema: type: string description: Search by product name or brand - name: limit in: query schema: type: integer default: 50 maximum: 1000 description: Number of results per page - name: offset in: query schema: type: integer default: 0 description: Pagination offset - name: fields in: query schema: type: string description: Comma-separated list of fields to return example: "id,name,price,brand,in_stock,image_url_full" responses: '200': description: Successful response content: application/json: schema: type: object properties: products: type: array items: $ref: '#/components/schemas/Product' total: type: integer description: Total number of products matching filters limit: type: integer offset: type: integer /products/{id}: get: tags: - Products summary: Get single product description: Retrieve detailed information about a specific product parameters: - name: id in: path required: true schema: type: integer responses: '200': description: Successful response content: application/json: schema: type: object properties: product: $ref: '#/components/schemas/Product' /categories: get: tags: - Categories summary: Get categories description: Retrieve flat list of categories parameters: - name: store_id in: query schema: type: integer description: Filter by store ID responses: '200': description: Successful response content: application/json: schema: type: object properties: categories: type: array items: $ref: '#/components/schemas/Category' /categories/tree: get: tags: - Categories summary: Get category tree description: Retrieve hierarchical category structure for a store parameters: - name: store_id in: query required: true schema: type: integer responses: '200': description: Successful response content: application/json: schema: type: object properties: tree: type: array items: allOf: - $ref: '#/components/schemas/Category' - type: object properties: children: type: array items: $ref: '#/components/schemas/Category' tags: - name: Authentication description: User authentication endpoints - name: Stores description: Dispensary store management - name: Products description: Product catalog and inventory - name: Categories description: Product categories and hierarchy