NUTBOM External API
Integrate NUTBOM recipe data, nutritional values, BOM trees, and compliance labels into your ERP, POS, or e-commerce platform.
Introduction
The NUTBOM API allows Enterprise and Pro customers to access their organisation's data programmatically via REST. It is designed for Machine-to-Machine communication — ERP sync, POS label printing, e-commerce nutrition display, and compliance auditing.
| Base URL | Format | Auth |
|---|---|---|
| https://dxespeybjglydorwxros.supabase.co/functions/v1/nutbom-api | JSON | OAuth 2.0 Bearer |
Authentication
The API uses OAuth 2.0 Client Credentials flow. Each request requires a short-lived Access Token (15 minutes).
Step 1 — Request an Access Token
Send your client_id and client_secret (from the NUTBOM Developer Portal) to the token endpoint:
| Parameter | Type | Required | Description |
|---|---|---|---|
| grant_type | string | required | Must be client_credentials |
| client_id | string | required | Your Client ID from the Developer Portal |
| client_secret | string | required | Your Client Secret — store securely, never in frontend code |
curl -X POST https://dxespeybjglydorwxros.supabase.co/functions/v1/nutbom-api/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=client_credentials&client_id=nutbom_abc123&client_secret=YOUR_SECRET"import requests
r = requests.post(
"https://dxespeybjglydorwxros.supabase.co/functions/v1/nutbom-api/oauth/token",
data={"grant_type":"client_credentials","client_id":"nutbom_abc123","client_secret":"YOUR_SECRET"}
)
token = r.json()["access_token"]const res = await fetch("https://dxespeybjglydorwxros.supabase.co/functions/v1/nutbom-api/oauth/token", {
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: "grant_type=client_credentials&client_id=nutbom_abc123&client_secret=YOUR_SECRET"
});
const { access_token } = await res.json();Token Response
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 900,
"scope": "recipes.read bom.read materials.read labels.read"
}
Step 2 — Use the Token
Include the token in every API request as a Authorization: Bearer header:
curl https://dxespeybjglydorwxros.supabase.co/functions/v1/nutbom-api/v1/materials \
-H "Authorization: Bearer YOUR_ACCESS_TOKEN"
Scopes
Scopes define what data the API key is allowed to access. They are set by your organisation admin in the Developer Portal and are embedded in the issued JWT.
| Scope | Description | Risk |
|---|---|---|
| recipes.read | View recipe names, versions, and status | Low |
| bom.read | Full ingredient tree including waste and evaporation | Medium |
| materials.read | Raw materials, allergens, and nutritional values | Low |
| labels.read | EU-compliant label data: ingredient statement, nutrition table, red labels | Low |
| nutrition.read | Calculated per-100g and per-serving nutritional values | Low |
| materials.prices.read | Price per kg for raw materials | Medium |
GET /v1/materials
Query Parameters
| Parameter | Type | Description | |
|---|---|---|---|
| updated_since | string (ISO-8601) | optional | Return only materials updated after this timestamp. Useful for delta sync with ERP. |
Response
{
"count": 2,
"data": [
{
"material_id": "d3f1...",
"name": "White Wheat Flour",
"name_he": "קמח חיטה לבן",
"allergens": { "gluten": "Contains" },
"price_per_kg": 2.40,
"status": "Approved",
"updated_at": "2026-03-15T10:22:00Z",
"nutritional_values": {
"energy_kcal": 364,
"proteins_g": 10.3,
"total_fats_g": 1.0,
"saturated_fats_g": 0.2,
"total_carbs_g": 76.3,
"sugars_g": 0.5,
"fibers_g": 2.7,
"sodium_mg": 2,
"moisture_percent": 14
}
}
]
}
GET /v1/recipes/{id}/bom
Path Parameters
| Parameter | Type | Description | |
|---|---|---|---|
| recipe_id | uuid | required | Recipe ID from NUTBOM |
Response
{
"recipe_id": "a1b2...",
"recipe_name": "Whole Wheat Sourdough",
"version": 3,
"status": "Approved",
"yield_factor": 0.95,
"total_input_weight_g": 1650,
"barcode": "7290001234567",
"ingredients": [
{
"type": "material",
"material_id": "d3f1...",
"sub_recipe_id": null,
"name": "White Wheat Flour",
"net_weight_g": 1000,
"waste_factor": 0.02,
"evaporation_rate": null
},
{
"type": "material",
"material_id": "e8a9...",
"sub_recipe_id": null,
"name": "Water",
"net_weight_g": 650,
"waste_factor": null,
"evaporation_rate": 0.15
}
]
}
waste_factor — proportion lost during prep (e.g. 0.02 = 2% trim loss). evaporation_rate — proportion of weight lost via evaporation during baking (e.g. 0.15 = 15%).
GET /v1/labels/{id}
Response
{
"recipe_id": "a1b2...",
"recipe_name": "Whole Wheat Sourdough",
"barcode": "7290001234567",
"ingredients_statement": "White Wheat Flour, Water, Salt, Sourdough Starter",
"nutrition_per_100g": {
"energy_kcal": 248,
"proteins": 8.2,
"total_fats": 0.9,
"saturated_fats": 0.1,
"total_carbs": 48.5,
"sugars": 1.2,
"fibers": 3.1,
"sodium": 380
},
"red_labels": {
"high_sodium": false,
"high_sugar": false,
"high_sat_fat": false
},
"allergens": {
"contains": ["gluten"],
"may_contain": []
}
}
Red label thresholds follow Israeli Ministry of Health Phase A (solid food): sodium > 600mg, sugar > 13.5g, saturated fat > 4g per 100g.
Webhooks
Instead of polling, NUTBOM can push real-time notifications to your server when data changes.
Supported Events
| Event | Triggered when |
|---|---|
| recipe.updated | A recipe is saved, approved, or its ingredients are changed |
| material.price_changed | The price per kg of a raw material is updated |
| recipe.compliance_changed | A recipe's red label status changes (e.g. ingredient swap changes sodium level) |
Payload format
{
"event": "recipe.updated",
"org_id": "org-uuid",
"recipe_id": "recipe-uuid",
"recipe_name": "Whole Wheat Sourdough",
"timestamp": "2026-04-12T08:30:00Z"
}
Security — HMAC Verification
Every webhook payload is signed with your Webhook Secret (found in the Developer Portal). Verify the X-Nutbom-Signature header to confirm the request came from NUTBOM:
import hmac, hashlib
def verify(secret: str, payload: bytes, signature: str) -> bool:
expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)const crypto = require("crypto");
function verify(secret, payload, signature) {
const expected = crypto.createHmac("sha256", secret).update(payload).digest("hex");
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}Test your endpoint
Use this cURL command to simulate a webhook delivery before going live:
curl -X POST https://your-server.com/webhook \
-H "Content-Type: application/json" \
-H "X-Nutbom-Signature: test_sig" \
-d '{"event":"recipe.updated","test":true}'
Error Codes
All errors return a consistent JSON body:
{ "error": { "status": 401, "code": "UNAUTHORIZED", "message": "Invalid or expired token" } }
| HTTP | Code | Meaning |
|---|---|---|
| 400 | INVALID_GRANT_TYPE | grant_type must be client_credentials |
| 400 | MISSING_CREDENTIALS | client_id or client_secret missing |
| 401 | UNAUTHORIZED | Token missing, invalid, or expired — request a new one |
| 401 | INVALID_CREDENTIALS | Wrong client_id or client_secret |
| 403 | FORBIDDEN | Token valid but missing required scope for this endpoint |
| 404 | NOT_FOUND | Recipe/resource not found, or no route matched |
| 422 | NO_INGREDIENTS | Recipe has no ingredients — cannot calculate nutrition |
| 429 | RATE_LIMIT_EXCEEDED | Too many requests — slow down |
| 500 | DB_ERROR | Internal error — contact support |
Rate Limits
| Plan | Limit | Window |
|---|---|---|
| Pro | 100 requests | per minute per organisation |
| Enterprise | 100 requests | per minute per organisation |
When the limit is exceeded the API returns HTTP 429. Implement exponential backoff — wait at least 60 seconds before retrying.