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.
For datasets larger than 500 products, use the File Upload workflow instead.
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
| Field | Type | Description |
|---|---|---|
provider | string | Provider name used for normalization |
total_input | integer | Number of products submitted |
total_output | integer | Number of products successfully normalized |
total_matched | integer | Products matched against existing CANN database |
total_errors | integer | Products that failed normalization |
processing_time_ms | number | Total processing time in milliseconds |
Normalized Product Fields
| Field | Type | Description |
|---|---|---|
uuid | string | CANN UUID if matched against existing products |
sku | string | CANN SKU hash if matched |
match_tier | string | Match confidence: primary (exact), secondary (fuzzy), or null |
display_product_name | string | Cleaned name (brand/weight stripped) |
raw_product_name | string | Original name from POS |
cann_product_category | string | Canonical category (Flower, Edible, Vape, etc.) |
subcategory | string | Detailed subcategory |
display_strain_type | string | Indica, Sativa, Hybrid, or CBD |
display_weight | string | Normalized weight string |
display_potency_list | string[] | Formatted potency values |
display_product_tag_list | string[] | Product attribute tags |
brand_id | integer | CANN brand database ID |
brand_name | string | Matched brand name |
brand_fuzzy_match_score | number | Match confidence (0.0 - 1.0) |
total_percentage_thc | number | THC percentage |
total_percentage_cbd | number | CBD percentage |
total_mg_thc | number | THC milligrams (edibles/tinctures) |
total_mg_cbd | number | CBD milligrams |
total_weight_in_grams | number | Weight in grams |
current_price | number | Retail price in dollars |
menu_provider | string | Source POS provider |
in_stock | boolean | Stock availability |
state | string | US state abbreviation |
Error Object
If any products fail normalization, they appear in the errors array:
| Field | Type | Description |
|---|---|---|
index | integer | Zero-based index of the failed product in the input array |
error | string | Description of what went wrong |
raw_product_name | string | Product 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.
