Batch Normalization

Normalize up to 500 raw POS products in a single synchronous request. Returns enriched products with canonical categories, brand matching, potency parsing, and optional UUID/SKU matching.


Request

POST https://api.cannmenus.com/v1/normalize/batch

Request Body

  • Name
    provider
    Type
    string
    Description

    POS provider name. Use a specific provider for best results, or "generic" for any POS system.

    "provider": "dutchie"
    "provider": "generic"
    
  • Name
    products
    Type
    RawProduct[]
    Description

    Array of raw products to normalize. Maximum 500 per request. See Raw Product below.

  • Name
    match_existing
    Type
    boolean
    Description

    Whether to attempt UUID/SKU matching against CANN's existing product database. Default: true.

  • Name
    include_raw
    Type
    boolean
    Description

    Whether to echo back the original raw data in each normalized product. Default: false.


Raw Product Object

Each product in the products array has these fields:

  • Name
    data
    Type
    object
    Description

    The raw product JSON from your POS provider. Structure varies by provider — send it exactly as your POS exports it.

  • Name
    state
    Type
    string
    Description

    US state abbreviation (e.g., "CO", "CA", "MA").

  • Name
    state_full
    Type
    string
    Description

    Full state name. Optional.

  • Name
    dispensary_id
    Type
    string
    Description

    External dispensary identifier from the POS system.

  • Name
    plantform_dispensary_id
    Type
    integer
    Description

    Internal CANN dispensary ID. Use if you know the retailer's CANN ID from the Retailers endpoint.

  • Name
    medical
    Type
    boolean
    Description

    Whether this product is from a medical menu. Default: false.

  • Name
    recreational
    Type
    boolean
    Description

    Whether this product is from a recreational menu. Default: true.

  • Name
    tax_included
    Type
    boolean
    Description

    Whether the price includes tax.


Example Requests

Dutchie Product

curl -X POST "https://api.cannmenus.com/v1/normalize/batch" \
  -H "X-Token: YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "dutchie",
    "products": [
      {
        "data": {
          "Name": "Happy Valley - Dog Patch - 3.5g",
          "type": "Flower",
          "THCContent": {"unit": "PERCENTAGE", "range": [24.0, 24.0]},
          "Prices": [52.00],
          "brand": "Happy Valley",
          "posId": "prod-12345",
          "strainType": "Hybrid"
        },
        "state": "MA",
        "dispensary_id": "store-123",
        "recreational": true
      }
    ],
    "match_existing": true
  }'

Generic POS Product

curl -X POST "https://api.cannmenus.com/v1/normalize/batch" \
  -H "X-Token: YOUR_API_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "provider": "generic",
    "products": [
      {
        "data": {
          "name": "Blue Dream Cartridge 1g",
          "brand": "Select",
          "category": "Vaporizers",
          "price": 45.00,
          "thc": "85%",
          "weight": "1g",
          "in_stock": true
        },
        "state": "CO",
        "recreational": true
      }
    ]
  }'

Python

import requests

API_URL = "https://api.cannmenus.com/v1"
headers = {"X-Token": "YOUR_API_TOKEN"}

response = requests.post(
    f"{API_URL}/normalize/batch",
    headers=headers,
    json={
        "provider": "generic",
        "products": [
            {
                "data": {
                    "name": "Sour Diesel 3.5g",
                    "brand": "Cookies",
                    "category": "Flower",
                    "price": 55.00,
                    "thc": "28%"
                },
                "state": "CA",
                "recreational": True,
            }
        ],
        "match_existing": True,
    },
)

result = response.json()
print(f"Normalized {result['total_output']} products, matched {result['total_matched']}")

for product in result["products"]:
    print(f"  {product['display_product_name']}{product['cann_product_category']}{product['display_weight']}")
    if product.get("brand_name"):
        print(f"    Brand: {product['brand_name']} (score: {product['brand_fuzzy_match_score']})")

Response

{
  "provider": "dutchie",
  "total_input": 1,
  "total_output": 1,
  "total_matched": 1,
  "total_errors": 0,
  "processing_time_ms": 142.5,
  "products": [
    {
      "uuid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
      "sku": "XkR9mN2pQw7vL4bT",
      "match_tier": "secondary",
      "display_product_name": "Dog Patch",
      "raw_product_name": "Happy Valley - Dog Patch - 3.5g",
      "cann_product_category": "Flower",
      "subcategory": null,
      "display_strain_type": "Hybrid",
      "display_weight": "3.5g",
      "display_potency_list": ["24% THC"],
      "display_product_tag_list": ["Hybrid"],
      "brand_id": 4521,
      "brand_name": "Happy Valley",
      "raw_brand_name": "Happy Valley",
      "brand_fuzzy_match_score": 0.95,
      "total_percentage_thc": 24.0,
      "total_weight_in_grams": 3.5,
      "current_price": 52.0,
      "menu_provider": "dutchie",
      "external_product_id": "prod-12345",
      "in_stock": true,
      "medical": false,
      "recreational": true,
      "state": "MA"
    }
  ],
  "errors": []
}

Response Summary Fields

FieldTypeDescription
providerstringProvider name used for normalization
total_inputintegerNumber of products submitted
total_outputintegerNumber of products successfully normalized
total_matchedintegerProducts matched against existing CANN database
total_errorsintegerProducts that failed normalization
processing_time_msnumberTotal processing time in milliseconds

Normalized Product Fields

FieldTypeDescription
uuidstringCANN UUID if matched against existing products
skustringCANN SKU hash if matched
match_tierstringMatch confidence: primary (exact), secondary (fuzzy), or null
display_product_namestringCleaned name (brand/weight stripped)
raw_product_namestringOriginal name from POS
cann_product_categorystringCanonical category (Flower, Edible, Vape, etc.)
subcategorystringDetailed subcategory
display_strain_typestringIndica, Sativa, Hybrid, or CBD
display_weightstringNormalized weight string
display_potency_liststring[]Formatted potency values
display_product_tag_liststring[]Product attribute tags
brand_idintegerCANN brand database ID
brand_namestringMatched brand name
brand_fuzzy_match_scorenumberMatch confidence (0.0 - 1.0)
total_percentage_thcnumberTHC percentage
total_percentage_cbdnumberCBD percentage
total_mg_thcnumberTHC milligrams (edibles/tinctures)
total_mg_cbdnumberCBD milligrams
total_weight_in_gramsnumberWeight in grams
current_pricenumberRetail price in dollars
menu_providerstringSource POS provider
in_stockbooleanStock availability
statestringUS state abbreviation

Error Object

If any products fail normalization, they appear in the errors array:

FieldTypeDescription
indexintegerZero-based index of the failed product in the input array
errorstringDescription of what went wrong
raw_product_namestringProduct name from raw data (if available)

Best Practices

Start with a Small Batch

Test with 10-20 products first to verify the output matches your expectations before sending larger batches.

Use Specific Providers When Possible

Dedicated normalizers (e.g., "dutchie") have full field mapping and produce better results than "generic" for supported POS systems.

Inspect Brand Match Scores

Products with brand_fuzzy_match_score below 0.8 may have less confident brand matches. Review these to ensure accuracy.

Handle Errors Gracefully

Some products may fail normalization (missing required fields, unrecognizable format). Always check the errors array and total_errors count.