Menu
shell python javascript

Introduction

Welcome to January AI's Nutritional Intelligence APIs documentation. These APIs put the food and metabolic-health intelligence behind January's own products into yours: a verified database of 54+ million foods, an AI vision model that reads meals from photos, and glucose prediction that works without a sensor — all through one REST API.

With a single integration, your product can:

Every endpoint speaks JSON over HTTPS. Code samples in shell, Python, and JavaScript run down the dark panel on the right — pick your language at the top of that panel.

New here? The Quickstart below takes you from API key to your first photo scan in about a minute, and Authentication explains the two identifying headers.

Endpoints at a glance

Endpoint Method and path x-partner-user-id
Food Search GET /v1.1/search/foods Optional
NLP Food Search GET /v1.1/search/foods/nlp Optional
Restaurant Search GET /v1.1/search/restaurants Not used
Restaurant Menu Search GET /v1.1/search/restaurants/menu Not used
Photo Scan POST /v1.1/vision/foods Required
Edit Photo Scan Results POST /v1.1/vision/foods/fix-ai Required
Food Alternatives POST /v1.1/food-alternatives/{foodId} Not used
Create Food Logs POST /v1.1/logs/foods Required
Retrieve Food Logs GET /v1.1/logs/foods Required
Delete Food Log DELETE /v1.1/logs/foods/{logId} Required
Glucose Prediction POST /v1.1/cgm/glucose-predict Not used — requires x-partner-timezone instead

Every request also carries the Authorization: Bearer YOUR_API_KEY header — see Authentication.

Quickstart

Scan a photo of a meal. Replace YOUR_API_KEY with your key and run it:

curl --location 'https://partners.january.ai/v1.1/vision/foods' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'x-partner-user-id: test-7f3c1a' \
--data '{
  "photoUrl": "https://i.imgur.com/bTQIGxf.png"
}'
import requests

url = 'https://partners.january.ai/v1.1/vision/foods'
headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_API_KEY',
    'x-partner-user-id': 'test-7f3c1a'
}
data = {
    "photoUrl": "https://i.imgur.com/bTQIGxf.png"
}

response = requests.post(url, headers=headers, json=data)
if response.ok:
    print(response.json())
else:
    print(response.status_code, response.text)
const axios = require('axios');

const config = {
  method: 'post',
  url: 'https://partners.january.ai/v1.1/vision/foods',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer YOUR_API_KEY',
    'x-partner-user-id': 'test-7f3c1a'
  },
  data: {
    photoUrl: 'https://i.imgur.com/bTQIGxf.png'
  }
};

axios(config)
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.log(error.response
      ? `${error.response.status} ${JSON.stringify(error.response.data)}`
      : error.message);
  });

A successful response looks like this — trimmed here; real responses include every detection and all ten nutrients per food:

{
  "mealName": "Breakfast Bowl",
  "totalNutrients": {
    "calories": {
      "value": 520,
      "unit": "calories"
    },
    "protein": {
      "value": 15,
      "unit": "grams"
    },
    "carbohydrates": {
      "value": 68,
      "unit": "grams"
    }
  },
  "detections": [
    {
      "confidenceScore": "high",
      "food": {
        "name": "Oatmeal",
        "id": 789012,
        "brandName": "",
        "nutrients": {
          "calories": {
            "value": 300,
            "unit": "calories"
          }
        },
        "servings": [
          {
            "id": 45678,
            "quantity": 1,
            "unit": "cup"
          }
        ]
      }
    }
  ]
}

The photo-scan request on the right is the fastest way to confirm your credentials work end to end: it sends one meal photo to Photo Scan and returns the foods January detects in it. Don't have an API key yet? Email us to request one.

The Python sample needs the requests package (pip install requests); the JavaScript sample needs axios (npm install axios) and runs on Node 18 or later. The error branch in each sample prints the HTTP status and body on failure — keep it, since that's where January's error messages (wrong key, malformed request) show up.

Two of this request's headers deserve a close look now:

Requests carry other headers as well — Content-Type: application/json here — but these two do the identifying. Every call needs the API key; each endpoint's Headers table says whether it needs the user ID too. See Authentication for what belongs in each.

Test values

While you're experimenting, use test- followed by a few random characters you make up — for example x-partner-user-id: test-7f3c1a. Pick one and keep reusing it, so your trial data stays in one place and stays separate from your colleagues'.

Note what this value is not: it isn't your name, your email, or anything that identifies a person. That's the same rule your production IDs follow, so the habit you build here is the one you want to ship.

There is no separate registration step for a test value: x-partner-user-id is the user's identity in January, and any stable ID you choose becomes that identity.

Authentication

Every user-scoped request carries both headers:

curl "https://partners.january.ai/v1.1/endpoint" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "x-partner-user-id: YOUR_PARTNER_USER_ID"
import requests

# Replace YOUR_API_KEY with your actual API key, and
# YOUR_PARTNER_USER_ID with the end user's stable ID from your system
headers = {
    'Authorization': 'Bearer YOUR_API_KEY',
    'x-partner-user-id': 'YOUR_PARTNER_USER_ID',
    'Content-Type': 'application/json'
}
const axios = require('axios');

// Replace YOUR_API_KEY with your actual API key, and
// YOUR_PARTNER_USER_ID with the end user's stable ID from your system
const headers = {
  'Authorization': 'Bearer YOUR_API_KEY',
  'x-partner-user-id': 'YOUR_PARTNER_USER_ID',
  'Content-Type': 'application/json'
};

A request to the January AI API can identify two things:

Identity Header Changes per request?
Your organization Authorization: Bearer YOUR_API_KEY No — the same key on every call
The end user you are acting on behalf of x-partner-user-id: YOUR_PARTNER_USER_ID Yes — one value per end user

Your API key says who is calling. x-partner-user-id says whose data this call is about. Endpoints that read or write a person's data need both.

Your API key

To get an API key, email us with your request.

Include the key on every request, in an Authorization header:

Authorization: Bearer YOUR_API_KEY

A missing or invalid API key returns 401 Unauthorized. 401 means the key and nothing else. If a request that should work still gets 401, check the header's exact form: the scheme is Bearer followed by a single space, and the key must be sent exactly as issued — the comparison is case-sensitive.

The end user: x-partner-user-id

Endpoints that act on a person — photo scans and food logs — are performed on behalf of one of your end users. The x-partner-user-id header names that user.

x-partner-user-id: YOUR_PARTNER_USER_ID

Send an identifier that is:

Any stable ID you choose works — it becomes that user's identity in January. There is no separate registration call to make first.

Why the header exists

The value scopes everything January stores about that person:

Send the wrong value and you read and write the wrong person's data. Omit it on an endpoint that requires it and the request fails with 400 Bad Request.

Each endpoint's Headers table below states whether x-partner-user-id is required for that endpoint.

Lifestyle Intelligence APIs

Look up any food a user might eat — by name, barcode, plain-English description, or photo — and search restaurant menus by dish and location, and suggest alternatives to any food.

Food Search (By Name or Barcode)

import requests

url = 'https://partners.january.ai/v1.1/search/foods'
params = {
    'query': 'banana',
    'category': 'branded'
}
headers = {
    'Authorization': 'Bearer YOUR_API_KEY',
    'x-partner-user-id': 'YOUR_PARTNER_USER_ID'
}

response = requests.get(url, params=params, headers=headers)
print(response.json())
curl --location 'https://partners.january.ai/v1.1/search/foods?query=banana&category=branded' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'x-partner-user-id: YOUR_PARTNER_USER_ID'
const axios = require('axios');

const config = {
  method: 'get',
  url: 'https://partners.january.ai/v1.1/search/foods',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'x-partner-user-id': 'YOUR_PARTNER_USER_ID'
  },
  params: {
    query: 'banana',
    category: 'branded'
  }
};

axios(config)
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.log(error);
  });

The above command returns JSON structured like this:

{
  "totalCount": 40,
  "items": [
    {
      "id": 84222716,
      "name": "Banana",
      "brand_name": "One",
      "protein": 2,
      "energy": 160,
      "carbs": 15,
      "fat": 10,
      "fat_total_saturated": 5,
      "net_carbs": 13,
      "sodium": 20,
      "sugars": 13,
      "added_sugars": 11,
      "gi": 34.9,
      "gl": 5.2,
      "fiber": 2,
      "potassium": 170,
      "cholesterol": 2.5,
      "photo_url": null,
      "servings": [
        {
          "id": 67943292,
          "quantity": 1,
          "unit": "bar",
          "scaling_factor": 1,
          "weight_grams": 60,
          "is_primary": true
        }
      ]
    }
  ]
}

Search for food items by name or barcode from January’s verified database of 54+ million foods, including branded, grocery, and restaurant items.

HTTP Request

GET https://partners.january.ai/v1.1/search/foods

Headers

Header Required Description
Authorization Yes Bearer token for authentication
x-partner-user-id No Identifies the end user this request acts on behalf of. See Authentication.

Query Parameters

Parameter Required Description
query Optional The search term for food items
category Optional The category of food items to filter results (branded, general, or recipe — case-sensitive)
upc Optional The UPC representing the barcode of a food item
limit Optional Integer 1–400. Default is 10. Name searches (general/branded) never return more than 40 items
offset Optional Integer, 0 or greater. Default is 0. For name searches (general/branded) only the first 40 matches are reachable, so an offset of 40 or more returns no items

Response Structure

Field Type Description
totalCount number Number of matching food items. Capped at 40 for name searches (general/branded)
items array Array of food item objects

Food Item Fields

Field Type Description
id number Food item ID
name string Food name
brand_name string or null Brand name, null or an empty string for generic foods
protein number or null Protein (g)
energy number or null Calories (kcal)
carbs number or null Total carbohydrates (g)
fat number or null Total fat (g)
fat_total_saturated number or null Saturated fat (g)
net_carbs number or null Net carbs: carbohydrate - fiber (g)
sodium number or null Sodium (mg)
sugars number or null Total sugars (g)
added_sugars number or null Added sugars (g)
gi number or null Glycemic index
gl number or null Glycemic load
fiber number or null Fiber (g)
potassium number or null Potassium (mg). Not returned for category=recipe
cholesterol number or null Cholesterol (mg). Not returned for category=recipe
photo_url string or null Food photo URL. Null or absent when no image is available
servings array Array of serving options

Serving Fields

Field Type Description
id number Serving ID
quantity number Serving quantity
unit string Serving unit description
scaling_factor number Scaling factor
weight_grams number or null Weight of the serving in grams
is_primary boolean Whether the returned serving is the food's primary serving
import requests

url = 'https://partners.january.ai/v1.1/search/foods/nlp'
params = {
    'query': '1 banana, 1 bowl of oatmeal, glass of orange juice'
}
headers = {
    'Authorization': 'Bearer YOUR_API_KEY',
    'x-partner-user-id': 'YOUR_PARTNER_USER_ID'
}

response = requests.get(url, params=params, headers=headers)
print(response.json())
curl --location 'https://partners.january.ai/v1.1/search/foods/nlp?query=1%20banana%2C%201%20bowl%20of%20oatmeal%2C%20glass%20of%20orange%20juice' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'x-partner-user-id: YOUR_PARTNER_USER_ID'
const axios = require('axios');

const config = {
  method: 'get',
  url: 'https://partners.january.ai/v1.1/search/foods/nlp',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'x-partner-user-id': 'YOUR_PARTNER_USER_ID'
  },
  params: {
    query: '1 banana, 1 bowl of oatmeal, glass of orange juice'
  }
};

axios(config)
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.log(error);
  });

The above command returns JSON structured like this — trimmed to one detection; totalNutrients sums every detected food, including ones not shown:

{
  "totalNutrients": {
    "calories": {
      "value": 435,
      "unit": "calories"
    },
    "protein": {
      "value": 8.5,
      "unit": "g"
    },
    "carbohydrates": {
      "value": 92,
      "unit": "g"
    },
    "totalFat": {
      "value": 18.3,
      "unit": "g"
    },
    "fiber": {
      "value": 6.1,
      "unit": "g"
    },
    "sodium": {
      "value": 320,
      "unit": "mg"
    }
  },
  "detections": [
    {
      "food": {
        "name": "Banana",
        "id": 123456,
        "brandName": "",
        "nutrients": {
          "calories": {
            "value": 105,
            "unit": "calories"
          },
          "protein": {
            "value": 1.3,
            "unit": "g"
          },
          "carbohydrates": {
            "value": 27,
            "unit": "g"
          },
          "totalFat": {
            "value": 0.4,
            "unit": "g"
          },
          "fiber": {
            "value": 3.1,
            "unit": "g"
          },
          "sodium": {
            "value": 1,
            "unit": "mg"
          }
        },
        "servings": [
          {
            "id": 78901,
            "quantity": 1,
            "unit": "medium (7\" to 7-7/8\" long)",
            "selected_quantity": 1
          }
        ]
      }
    }
  ]
}

Search for multiple foods at once by describing them in plain English — via voice or text.

HTTP Request

GET https://partners.january.ai/v1.1/search/foods/nlp

Headers

Header Required Description
Authorization Yes Bearer token for authentication
x-partner-user-id No Identifies the end user this request acts on behalf of. See Authentication.

Query Parameters

Parameter Required Description
query Yes The search term for food items in natural language. Required for meaningful results, but not enforced by the service — omitting it fails silently (no validation error) instead of returning a 400

Response Structure

Field Type Description
totalNutrients object Sum of scaled nutrients across all detected foods
detections array One entry per detected food item

Detection Object

Field Type Description
food.name string Food name
food.id number Food item ID
food.brandName string Brand name, or empty string for generic foods
food.nutrients object Scaled nutrients for this detection (see nutrient keys below)
food.servings array Matched servings for the detection

Nutrient Keys

All nutrient values are objects with value (number) and unit (string). Fields are omitted when the source data is not available, and also when the value is exactly zero.

Key Unit Description
calories calories Energy (kcal)
protein g Protein
carbohydrates g Total carbohydrates
netCarbohydrates g Net carbs (carbohydrate - fiber)
totalFat g Total fat
saturatedFat g Saturated fat
fiber g Fiber
totalSugars g Total sugars
addedSugars g Added sugars
sodium mg Sodium

Serving Fields

Field Type Description
id number Serving ID
quantity number Serving quantity
unit string Serving unit description
selected_quantity number Detected quantity for the serving. Optional — omitted when the serving has no gram weight
import requests

url = 'https://partners.january.ai/v1.1/search/restaurants'
params = {
    'query': 'mcdonalds',
    'lat': 37.549,
    'lon': -121.989
}
headers = {
    'Authorization': 'Bearer YOUR_API_KEY'
}

response = requests.get(url, params=params, headers=headers)
print(response.json())
curl --location 'https://partners.january.ai/v1.1/search/restaurants?query=mcdonalds&lat=37.549&lon=-121.989' \
--header 'Authorization: Bearer YOUR_API_KEY'
const axios = require('axios');

const config = {
  method: 'get',
  url: 'https://partners.january.ai/v1.1/search/restaurants',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY'
  },
  params: {
    query: 'mcdonalds',
    lat: 37.549,
    lon: -121.989
  }
};

axios(config)
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.log(error);
  });

The above command returns JSON structured like this:

{
  "items": [
    {
      "type": "restaurant",
      "id": "53fc3b8a-e6bf-404d-83c8-9f42124d1bee",
      "name": "McDonald's",
      "is_chain": false,
      "distance": 8,
      "city": "San Francisco",
      "address1": "123 Main Street",
      "address2": "Suite 100"
    }
  ]
}

Search for restaurants — local spots and chains — by name, ranked by distance when you provide the user's coordinates.

When you send coordinates, a generic dish name like pizza — or any query that matches no restaurant — returns menu items instead. Those items have "type": "menu_item" and the shape described under Restaurant Menu Search, so check type on each item before reading it. Without coordinates, results are always restaurants.

HTTP Request

GET https://partners.january.ai/v1.1/search/restaurants

Headers

Header Required Description
Authorization Yes Bearer token for authentication

Query Parameters

Parameter Required Description
query Yes Restaurant name to search for. Pass an empty value to list the nearest restaurants.
lat Optional Latitude for proximity ranking, −90 to 90. Must be sent together with lon
lon Optional Longitude for proximity ranking, −180 to 180. Must be sent together with lat
distance Optional Search radius in meters. Default is 16093 (~10 miles), maximum 17000 — larger values return a 400 rather than being clamped
limit Optional Maximum number of results to return. Default is 50, maximum 100 — larger values return a 400

Response Structure

The response includes:

import requests

url = 'https://partners.january.ai/v1.1/search/restaurants/menu'
params = {
    'query': 'burger',
    'lat': 37.549,
    'lon': -121.989
}
headers = {
    'Authorization': 'Bearer YOUR_API_KEY'
}

response = requests.get(url, params=params, headers=headers)
print(response.json())
curl --location 'https://partners.january.ai/v1.1/search/restaurants/menu?query=burger&lat=37.549&lon=-121.989' \
--header 'Authorization: Bearer YOUR_API_KEY'
const axios = require('axios');

const config = {
  method: 'get',
  url: 'https://partners.january.ai/v1.1/search/restaurants/menu',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY'
  },
  params: {
    query: 'burger',
    lat: 37.549,
    lon: -121.989
  }
};

axios(config)
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.log(error);
  });

The above command returns JSON structured like this:

{
  "items": [
    {
      "type": "menu_item",
      "id": "228990954",
      "name": "burger",
      "restaurant_name": "morning due cafe",
      "is_chain": false,
      "data_source_id": 12,
      "source_original_id": "ngxZD7tEA1cQGCuOYBF05Ckft2ZP3z3Ja88wVaXYqqr7P7F3QiGFN1BSCl6H0Bt_5iN7y",
      "protein": 48,
      "energy": 800,
      "carbs": 40,
      "net_carbs": 38,
      "sugars": 6,
      "added_sugars": null,
      "fat": 45,
      "gi": 48.507698,
      "gl": 36.67089,
      "fiber": 2,
      "photo_url": "https://cdn-img.ai/23361362e6a706567",
      "servings": [
        {
          "id": 189343592,
          "scaling_factor": 1,
          "quantity": 1,
          "unit": "serving",
          "weight_grams": null,
          "is_primary": true
        }
      ],
      "distance": 124
    }
  ]
}

Search for dishes across the menus of restaurants near a location — every "burger" within ten miles, with nutrition data for each match.

HTTP Request

GET https://partners.january.ai/v1.1/search/restaurants/menu

Headers

Header Required Description
Authorization Yes Bearer token for authentication

Query Parameters

Parameter Required Description
query Yes Dish or restaurant name to search for
lat Yes Latitude to search around, −90 to 90
lon Yes Longitude to search around, −180 to 180
distance Optional Search radius in meters. Default is 16093 (~10 miles), maximum 17000 — larger values return a 400 rather than being clamped
limit Optional Maximum number of results to return. Default is 50, maximum 100 — larger values return a 400

Response Structure

The response includes:

Photo Scan

import requests
import json

url = 'https://partners.january.ai/v1.1/vision/foods'
headers = {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
    'x-partner-user-id': 'YOUR_PARTNER_USER_ID'
}
data = {
    "photoUrl": "https://i.imgur.com/bTQIGxf.png"
}

response = requests.post(url, headers=headers, json=data)
print(response.json())
curl --location 'https://partners.january.ai/v1.1/vision/foods' \
--header 'Content-Type: application/json' \
--header 'x-partner-user-id: YOUR_PARTNER_USER_ID' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
  "photoUrl": "https://i.imgur.com/bTQIGxf.png"
}'
const axios = require('axios');

const config = {
  method: 'post',
  url: 'https://partners.january.ai/v1.1/vision/foods',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
    'x-partner-user-id': 'YOUR_PARTNER_USER_ID'
  },
  data: {
    photoUrl: 'https://i.imgur.com/bTQIGxf.png'
  }
};

axios(config)
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.log(error);
  });

The above command returns JSON structured like this — trimmed here; real responses include every detection and all ten nutrients per food:

{
  "mealName": "Breakfast Bowl",
  "totalNutrients": {
    "calories": {
      "value": 520,
      "unit": "calories"
    },
    "protein": {
      "value": 15,
      "unit": "grams"
    },
    "carbohydrates": {
      "value": 68,
      "unit": "grams"
    }
  },
  "detections": [
    {
      "confidenceScore": "high",
      "food": {
        "name": "Oatmeal",
        "id": 789012,
        "brandName": "",
        "nutrients": {
          "calories": {
            "value": 300,
            "unit": "calories"
          }
        },
        "servings": [
          {
            "id": 45678,
            "quantity": 1,
            "unit": "cup"
          }
        ]
      }
    }
  ]
}

Scan a photo of a meal to identify and retrieve nutritional data for the foods detected.

HTTP Request

POST https://partners.january.ai/v1.1/vision/foods

Headers

Header Required Description
Authorization Yes Bearer token for authentication
Content-Type Yes Must be application/json
x-partner-user-id Yes Identifies the end user this request acts on behalf of. See Authentication.

Request Body

Parameter Required Description
photoUrl No The URL of the image to be analyzed
photoBase64 No Base64 encoded image data (if not using photoUrl). Raw base64 is treated as JPEG; for other formats, send a data URL (data:image/png;base64,...)

Photo Requirements

Response Structure

The response includes:

Edit Photo Scan Results

import requests
import json

url = 'https://partners.january.ai/v1.1/vision/foods/fix-ai'
headers = {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
    'x-partner-user-id': 'YOUR_PARTNER_USER_ID'
}
data = {
  "mealName": "Breakfast Bowl",
  "detections": [
    {
      "confidenceScore": "high",
      "food": {
        "name": "Oatmeal",
        "id": 789012,
        "brandName": "",
        "nutrients": {
          "calories": {
            "value": 300,
            "unit": "calories"
          },
          "protein": {
            "value": 10.5,
            "unit": "grams"
          },
          "carbohydrates": {
            "value": 54,
            "unit": "grams"
          },
          "netCarbohydrates": {
            "value": 46,
            "unit": "grams"
          },
          "totalFat": {
            "value": 5.5,
            "unit": "grams"
          },
          "saturatedFat": {
            "value": 1,
            "unit": "grams"
          },
          "fiber": {
            "value": 8,
            "unit": "grams"
          },
          "totalSugars": {
            "value": 1.5,
            "unit": "grams"
          },
          "addedSugars": {
            "value": 0,
            "unit": "grams"
          },
          "sodium": {
            "value": 9,
            "unit": "milligrams"
          }
        },
        "servings": [
          {
            "id": 45678,
            "quantity": 1,
            "unit": "cup"
          }
        ]
      }
    }
  ],
  "userInput": "change oatmeal to steel-cut oats"
}

response = requests.post(url, headers=headers, json=data)
print(response.json())
curl --location 'https://partners.january.ai/v1.1/vision/foods/fix-ai' \
--header 'Content-Type: application/json' \
--header 'x-partner-user-id: YOUR_PARTNER_USER_ID' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
  "mealName": "Breakfast Bowl",
  "detections": [
    {
      "confidenceScore": "high",
      "food": {
        "name": "Oatmeal",
        "id": 789012,
        "brandName": "",
        "nutrients": {
          "calories": {
            "value": 300,
            "unit": "calories"
          },
          "protein": {
            "value": 10.5,
            "unit": "grams"
          },
          "carbohydrates": {
            "value": 54,
            "unit": "grams"
          },
          "netCarbohydrates": {
            "value": 46,
            "unit": "grams"
          },
          "totalFat": {
            "value": 5.5,
            "unit": "grams"
          },
          "saturatedFat": {
            "value": 1,
            "unit": "grams"
          },
          "fiber": {
            "value": 8,
            "unit": "grams"
          },
          "totalSugars": {
            "value": 1.5,
            "unit": "grams"
          },
          "addedSugars": {
            "value": 0,
            "unit": "grams"
          },
          "sodium": {
            "value": 9,
            "unit": "milligrams"
          }
        },
        "servings": [
          {
            "id": 45678,
            "quantity": 1,
            "unit": "cup"
          }
        ]
      }
    }
  ],
  "userInput": "change oatmeal to steel-cut oats"
}'
const axios = require('axios');

const config = {
  method: 'post',
  url: 'https://partners.january.ai/v1.1/vision/foods/fix-ai',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
    'x-partner-user-id': 'YOUR_PARTNER_USER_ID'
  },
  data: {
    "mealName": "Breakfast Bowl",
    "detections": [
      {
        "confidenceScore": "high",
        "food": {
          "name": "Oatmeal",
          "id": 789012,
          "brandName": "",
          "nutrients": {
            "calories": {
              "value": 300,
              "unit": "calories"
            },
            "protein": {
              "value": 10.5,
              "unit": "grams"
            },
            "carbohydrates": {
              "value": 54,
              "unit": "grams"
            },
            "netCarbohydrates": {
              "value": 46,
              "unit": "grams"
            },
            "totalFat": {
              "value": 5.5,
              "unit": "grams"
            },
            "saturatedFat": {
              "value": 1,
              "unit": "grams"
            },
            "fiber": {
              "value": 8,
              "unit": "grams"
            },
            "totalSugars": {
              "value": 1.5,
              "unit": "grams"
            },
            "addedSugars": {
              "value": 0,
              "unit": "grams"
            },
            "sodium": {
              "value": 9,
              "unit": "milligrams"
            }
          },
          "servings": [
            {
              "id": 45678,
              "quantity": 1,
              "unit": "cup"
            }
          ]
        }
      }
    ],
    "userInput": "change oatmeal to steel-cut oats"
  }
};

axios(config)
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.log(error);
  });

The above command returns the corrected scan — the detection is now steel-cut oats, and the totals match it:

{
  "mealName": "Breakfast Bowl",
  "totalNutrients": {
    "calories": {
      "value": 310,
      "unit": "calories"
    },
    "protein": {
      "value": 11,
      "unit": "grams"
    },
    "carbohydrates": {
      "value": 56,
      "unit": "grams"
    },
    "netCarbohydrates": {
      "value": 48,
      "unit": "grams"
    },
    "totalFat": {
      "value": 5,
      "unit": "grams"
    },
    "saturatedFat": {
      "value": 1,
      "unit": "grams"
    },
    "fiber": {
      "value": 8,
      "unit": "grams"
    },
    "totalSugars": {
      "value": 1,
      "unit": "grams"
    },
    "addedSugars": {
      "value": 0,
      "unit": "grams"
    },
    "sodium": {
      "value": 5,
      "unit": "milligrams"
    }
  },
  "detections": [
    {
      "confidenceScore": "high",
      "food": {
        "name": "Steel-Cut Oats",
        "id": 789013,
        "brandName": "",
        "nutrients": {
          "calories": {
            "value": 310,
            "unit": "calories"
          },
          "protein": {
            "value": 11,
            "unit": "grams"
          },
          "carbohydrates": {
            "value": 56,
            "unit": "grams"
          },
          "netCarbohydrates": {
            "value": 48,
            "unit": "grams"
          },
          "totalFat": {
            "value": 5,
            "unit": "grams"
          },
          "saturatedFat": {
            "value": 1,
            "unit": "grams"
          },
          "fiber": {
            "value": 8,
            "unit": "grams"
          },
          "totalSugars": {
            "value": 1,
            "unit": "grams"
          },
          "addedSugars": {
            "value": 0,
            "unit": "grams"
          },
          "sodium": {
            "value": 5,
            "unit": "milligrams"
          }
        },
        "servings": [
          {
            "id": 45679,
            "quantity": 1,
            "unit": "cup"
          }
        ]
      }
    }
  ]
}

Lets a user correct a photo scan in natural language, by voice or text. For example, after scanning a chicken and rice bowl and spotting a couple of inaccuracies, a user might say:

"It was actually 1 cup of chicken. It was also white rice, not brown rice."

The API parses this input and updates the scan results accordingly.

HTTP Request

POST https://partners.january.ai/v1.1/vision/foods/fix-ai

Headers

Header Required Description
Authorization Yes Bearer token for authentication
Content-Type Yes Must be application/json
x-partner-user-id Yes Identifies the end user this request acts on behalf of. See Authentication.

Request Body

Parameter Required Description
mealName Yes Name of the meal to edit
detections Yes Array of detected food items to edit
detections[].food.nutrients Yes Must contain all ten macros: calories, protein, carbohydrates, netCarbohydrates, totalFat, saturatedFat, fiber, totalSugars, addedSugars, and sodium
detections[].food.servings Yes Must be present and non-empty
userInput Yes Text representation of changes to make

Response Structure

The response includes:

Food Alternatives

import requests
import json

url = 'https://partners.january.ai/v1.1/food-alternatives/70373460'
headers = {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json'
}
data = {
    "dietRestrictions": ["Gluten"],
    "dietPreferences": ["Vegetarian"]
}

response = requests.post(url, headers=headers, json=data)
print(response.json())
curl --location 'https://partners.january.ai/v1.1/food-alternatives/70373460' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
  "dietRestrictions": ["Gluten"],
  "dietPreferences": ["Vegetarian"]
}'
const axios = require('axios');

const config = {
  method: 'post',
  url: 'https://partners.january.ai/v1.1/food-alternatives/70373460',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json'
  },
  data: {
    dietRestrictions: ["Gluten"],
    dietPreferences: ["Vegetarian"]
  }
};

axios(config)
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.log(error);
  });

The above command returns JSON structured like this:

{
  "alternatives": [
    {
      "food": {
        "name": "brown rice",
        "id": 70372230,
        "brandName": "",
        "nutrients": {
          "calories": {
            "value": 109.2,
            "unit": "calories"
          },
          "protein": {
            "value": 2.26,
            "unit": "grams"
          },
          "carbohydrates": {
            "value": 22.92,
            "unit": "grams"
          },
          "totalFat": {
            "value": 0.81,
            "unit": "grams"
          },
          "fiber": {
            "value": 1.75,
            "unit": "grams"
          }
        },
        "servings": [
          {
            "id": 34113801,
            "quantity": 0.5,
            "unit": "cup"
          }
        ]
      }
    },
    {
      "food": {
        "name": "wild rice",
        "id": 70383579,
        "brandName": "",
        "nutrients": {
          "calories": {
            "value": 165.64,
            "unit": "calories"
          },
          "protein": {
            "value": 6.54,
            "unit": "grams"
          },
          "carbohydrates": {
            "value": 35,
            "unit": "grams"
          },
          "totalFat": {
            "value": 0.56,
            "unit": "grams"
          },
          "fiber": {
            "value": 2.95,
            "unit": "grams"
          }
        },
        "servings": [
          {
            "id": 34201413,
            "quantity": 1,
            "unit": "cup"
          }
        ]
      }
    }
  ]
}

Suggest alternatives for a food the user is about to eat or log. Given a food ID and the user's dietary restrictions and preferences, returns similar foods, best match first, each with a recommended serving size calibrated using serving weight and available glycemic-load data — for example, brown rice and wild rice as swaps for white rice.

The food ID comes from any discovery endpoint: Food Search, NLP Food Search, and Photo Scan all return the same food IDs.

HTTP Request

POST https://partners.january.ai/v1.1/food-alternatives/{foodId}

Headers

Header Required Description
Authorization Yes Bearer token for authentication
Content-Type Yes Must be application/json

URL Parameters

Parameter Description
foodId The ID of the food to find alternatives for. An unknown ID returns 404 Not Found

Request Body

Parameter Required Description
dietRestrictions Yes Array of the user's allergies and intolerances, from Dietary Restrictions. Send ["None"] if the user has no restrictions.
dietPreferences Yes Array of the user's diet styles, from Dietary Preferences. Send ["None"] if the user has no preferences.

Dietary Restrictions

dietRestrictions accepts: 'None', 'Gluten', 'Lactose', 'Yeast', 'Tree nuts', 'Peanuts', 'Dairy', 'Eggs', 'Sulfites', 'Soy', 'Wheat', 'Shellfish', 'Fish', 'Mushrooms', 'Sesame', 'Monosodium glutamate (MSG)', 'Caffeine', 'FODMAPs'.

Dietary Preferences

dietPreferences accepts: 'None', 'Vegetarian', 'Vegan', 'Keto', 'Paleo', 'Pescatarian', 'Low carbohydrate', 'High protein', 'Kosher'.

These values are case-sensitive strings and must match exactly as written — 'vegan' wouldn't work; send 'Vegan'.

Response Structure

Field Type Description
alternatives array Suggested alternatives for the requested food, best match first. Each entry wraps a food object.

An empty alternatives array is a valid response, not an error: it means January found no suitable alternative for that food under the given restrictions and preferences.

Alternative Food Fields

Field Type Description
name string Food name
id number Food item ID, accepted anywhere a food ID is — for example, Glucose Prediction
brandName string Brand name, empty string for generic foods
nutrients object calories, protein, carbohydrates, totalFat, and fiber, each as { "value", "unit" }
servings array A single recommended serving with id, quantity, and unit — the quantity is calibrated by January, not one of the food's standard serving options

Food Logging APIs

Write to and read from each user's food diary.

Create Food Logs

import requests
import json

url = 'https://partners.january.ai/v1.1/logs/foods'
headers = {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
    'x-partner-user-id': 'YOUR_PARTNER_USER_ID'
}
data = {
    "foods": [
        {
            "id": 101963552,
            "serving": {
                "id": 68051535,
                "quantity": 1.4
            }
        }
    ],
    "timestampUtc": "2024-09-13T11:34:56Z"
}

response = requests.post(url, headers=headers, json=data)
print(response.json())
curl --location 'https://partners.january.ai/v1.1/logs/foods' \
--header 'Content-Type: application/json' \
--header 'x-partner-user-id: YOUR_PARTNER_USER_ID' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--data '{
  "foods": [
    {
      "id": 101963552,
      "serving": {
        "id": 68051535,
        "quantity": 1.4
      }
    }
  ],
  "timestampUtc": "2024-09-13T11:34:56Z"
}'
const axios = require('axios');

const config = {
  method: 'post',
  url: 'https://partners.january.ai/v1.1/logs/foods',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
    'x-partner-user-id': 'YOUR_PARTNER_USER_ID'
  },
  data: {
    foods: [
      {
        id: 101963552,
        serving: {
          id: 68051535,
          quantity: 1.4
        }
      }
    ],
    timestampUtc: "2024-09-13T11:34:56Z"
  }
};

axios(config)
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.log(error);
  });

The above command returns 200 OK with the created log — each food hydrated with its name, scaled nutrients, and serving details:

{
  "id": "78129823-8ba2-4183-b13b-71f0e963c606",
  "foods": [
    {
      "id": 101963552,
      "name": "Greek Yogurt, Plain, Whole Milk",
      "brandName": null,
      "imageUrl": null,
      "glycemicIndex": 11.3,
      "glycemicLoad": 1.4,
      "nutrients": {
        "calories": {
          "value": 308,
          "unit": "calories"
        },
        "protein": {
          "value": 28,
          "unit": "g"
        },
        "carbohydrates": {
          "value": 12.6,
          "unit": "g"
        },
        "netCarbohydrates": {
          "value": 12.6,
          "unit": "g"
        },
        "totalFat": {
          "value": 15.4,
          "unit": "g"
        },
        "saturatedFat": {
          "value": 9.8,
          "unit": "g"
        },
        "totalSugars": {
          "value": 12.6,
          "unit": "g"
        },
        "sodium": {
          "value": 119,
          "unit": "mg"
        },
        "calcium": {
          "value": 322,
          "unit": "mg"
        },
        "potassium": {
          "value": 420,
          "unit": "mg"
        }
      },
      "consumedServing": {
        "id": 68051535,
        "quantity": 1.4
      },
      "servingDetails": {
        "id": 68051535,
        "quantity": 1,
        "unit": "cup",
        "weightGrams": 245
      }
    }
  ],
  "timestampUtc": "2024-09-13T11:34:56Z",
  "name": null
}

Log one or more foods to a user's food diary at a given time. Food and serving IDs come from the search and photo scan endpoints.

HTTP Request

POST https://partners.january.ai/v1.1/logs/foods

Headers

Header Required Description
Authorization Yes Bearer token for authentication
Content-Type Yes Must be application/json
x-partner-user-id Yes Identifies the end user this request acts on behalf of. See Authentication.

Request Body

Parameter Required Description
foods Yes Array of food items to be logged — at least one entry
foods[].id Yes Unique identifier of the food item, as a JSON number (a quoted "123" returns a 400)
foods[].serving Yes Serving details of the food item
foods[].serving.id Yes Serving size identifier, as a JSON number
foods[].serving.quantity Yes Amount of the serving consumed
timestampUtc Optional UTC timestamp when food was consumed, ending in a literal Z — e.g. 2024-09-13T11:34:56Z (seconds and fractional seconds optional). A timestamp with a UTC offset (+02:00), without the Z, or as a date only returns a 400. When omitted, the log is stamped with the current server time
name Optional Name of the meal. Omit optional fields entirely rather than sending null — explicit nulls return a 400

Response Structure

Success is 200 OK with the created log:

Field Type Description
id string Unique ID of the created log. Save it — it's the {logId} for Delete Food Log
foods array The logged foods, hydrated with names, nutrients, and serving details (see below)
timestampUtc string The timestamp the log was stored with
name string or null Meal name; null when name was not sent

Logged Food Fields

Field Type Description
id number Food item ID
name string Food name
brandName string or null Brand name; null for generic foods
imageUrl string or null Food photo URL; null when no image is available
glycemicIndex number or null Glycemic index
glycemicLoad number or null Glycemic load
nutrients object Nutrients scaled to the consumed serving and quantity (see below)
consumedServing object Echo of what you logged: id and quantity
servingDetails object The consumed serving's definition: id, unit, quantity, and weightGrams (null when unknown)

nutrients values are objects with value (number) and unit (string), scaled to the consumed quantity. Possible keys: calories, protein, carbohydrates, netCarbohydrates, totalFat, transFat, saturatedFat, fiber, totalSugars, addedSugars, cholesterol, calcium, iron, potassium, sodium, and vitaminD. Units are calories, g, or mg, except iron (mcg) and vitaminD (IU). As in NLP Food Search, keys are omitted when the source data is unavailable and when the value is exactly zero.

Retrieve Food Logs

import requests

url = 'https://partners.january.ai/v1.1/logs/foods'
params = {
    'start': '2023-09-12',
    'end': '2024-09-15'
}
headers = {
    'Authorization': 'Bearer YOUR_API_KEY',
    'x-partner-user-id': 'YOUR_PARTNER_USER_ID'
}

response = requests.get(url, params=params, headers=headers)
print(response.json())
curl --location 'https://partners.january.ai/v1.1/logs/foods?start=2023-09-12&end=2024-09-15' \
--header 'x-partner-user-id: YOUR_PARTNER_USER_ID' \
--header 'Authorization: Bearer YOUR_API_KEY'
const axios = require('axios');

const config = {
  method: 'get',
  url: 'https://partners.january.ai/v1.1/logs/foods',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'x-partner-user-id': 'YOUR_PARTNER_USER_ID'
  },
  params: {
    start: '2023-09-12',
    end: '2024-09-15'
  }
};

axios(config)
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.log(error);
  });

The above command returns 200 OK with a JSON array of log objects — the same shape Create Food Logs returns. Trimmed here to one log with one food:

[
  {
    "id": "78129823-8ba2-4183-b13b-71f0e963c606",
    "foods": [
      {
        "id": 101963552,
        "name": "Greek Yogurt, Plain, Whole Milk",
        "brandName": null,
        "imageUrl": null,
        "glycemicIndex": 11.3,
        "glycemicLoad": 1.4,
        "nutrients": {
          "calories": {
            "value": 308,
            "unit": "calories"
          },
          "protein": {
            "value": 28,
            "unit": "g"
          },
          "carbohydrates": {
            "value": 12.6,
            "unit": "g"
          }
        },
        "consumedServing": {
          "id": 68051535,
          "quantity": 1.4
        },
        "servingDetails": {
          "id": 68051535,
          "quantity": 1,
          "unit": "cup",
          "weightGrams": 245
        }
      }
    ],
    "timestampUtc": "2024-09-13T11:34:56Z",
    "name": null
  }
]

Retrieve a user's food logs over a date range.

HTTP Request

GET https://partners.january.ai/v1.1/logs/foods

Headers

Header Required Description
Authorization Yes Bearer token for authentication
x-partner-user-id Yes Identifies the end user this request acts on behalf of. See Authentication.

Query Parameters

Parameter Required Description
start Yes Start date, strictly YYYY-MM-DD (zero-padded) — other formats return a 400
end Yes End date, strictly YYYY-MM-DD (zero-padded), and strictly after startstart equal to end returns a 400

Both dates are read as UTC calendar days, and both ends are inclusive: the range runs from start at 00:00:00 UTC through end at 23:59:59 UTC. Because end must be after start and both days are included, the smallest possible window covers two days — to read a single day's logs, set end to the following day and filter by timestampUtc on your side.

Response Structure

Success is 200 OK with a JSON array of log objects, each shaped exactly like the Create Food Logs response: id, foods, timestampUtc, and name. An empty array ([]) means no logs in the range — it is not an error.

Delete Food Log

import requests

url = 'https://partners.january.ai/v1.1/logs/foods/78129823-8ba2-4183-b13b-71f0e963c606'
headers = {
    'Authorization': 'Bearer YOUR_API_KEY',
    'x-partner-user-id': 'YOUR_PARTNER_USER_ID'
}

response = requests.delete(url, headers=headers)
print(response.status_code)
curl --location --request DELETE 'https://partners.january.ai/v1.1/logs/foods/78129823-8ba2-4183-b13b-71f0e963c606' \
--header 'x-partner-user-id: YOUR_PARTNER_USER_ID' \
--header 'Authorization: Bearer YOUR_API_KEY'
const axios = require('axios');

const config = {
  method: 'delete',
  url: 'https://partners.january.ai/v1.1/logs/foods/78129823-8ba2-4183-b13b-71f0e963c606',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'x-partner-user-id': 'YOUR_PARTNER_USER_ID'
  }
};

axios(config)
  .then(response => {
    console.log(response.status);
  })
  .catch(error => {
    console.log(error);
  });

Delete a single food log entry by its ID.

HTTP Request

DELETE https://partners.january.ai/v1.1/logs/foods/{logId}

URL Parameters

Parameter Description
logId The unique identifier of the food log to delete

Headers

Header Required Description
Authorization Yes Bearer token for authentication
x-partner-user-id Yes Identifies the end user this request acts on behalf of. See Authentication.

Response Structure

Success is 200 OK with an empty body. Deletion is idempotent: deleting an ID that doesn't exist — or was already deleted — also returns 200 OK, so retrying a delete after a timeout is safe.

Glucose Insights APIs

Predict how a meal will move a user's glucose — before they take the first bite. You pass the user's profile in the request body on every call — nothing is stored, and no setup is required.

Glucose Prediction

import requests
import json

url = 'https://partners.january.ai/v1.1/cgm/glucose-predict'
headers = {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
    'x-partner-timezone': 'America/Los_Angeles'
}
data = {
    "userProfile": {
        "age": 25,
        "gender": "male",
        "height": 65,
        "weight": 165,
        "activity_level": "very_active",
        "medical_conditions": ["Type 2 diabetes"]
    },
    "startTime": "2025-04-09T19:06:12Z",
    "foods": [
        {
            "id": 70380652,
            "serving": {
                "id": 34176931,
                "quantity": 1
            }
        }
    ]
}

response = requests.post(url, headers=headers, json=data)
print(response.json())
curl --location 'https://partners.january.ai/v1.1/cgm/glucose-predict' \
--header 'Content-Type: application/json' \
--header 'Authorization: Bearer YOUR_API_KEY' \
--header 'x-partner-timezone: America/Los_Angeles' \
--data '{
  "userProfile": {
    "age": 25,
    "gender": "male",
    "height": 65,
    "weight": 165,
    "activity_level": "very_active",
    "medical_conditions": ["Type 2 diabetes"]
  },
  "startTime": "2025-04-09T19:06:12Z",
  "foods": [
    {
      "id": 70380652,
      "serving": {
        "id": 34176931,
        "quantity": 1
      }
    }
  ]
}'
const axios = require('axios');

const config = {
  method: 'post',
  url: 'https://partners.january.ai/v1.1/cgm/glucose-predict',
  headers: {
    'Authorization': 'Bearer YOUR_API_KEY',
    'Content-Type': 'application/json',
    'x-partner-timezone': 'America/Los_Angeles'
  },
  data: {
    userProfile: {
      age: 25,
      gender: "male",
      height: 65,
      weight: 165,
      activity_level: "very_active",
      medical_conditions: ["Type 2 diabetes"]
    },
    startTime: "2025-04-09T19:06:12Z",
    foods: [
      {
        id: 70380652,
        serving: {
          id: 34176931,
          quantity: 1
        }
      }
    ]
  }
};

axios(config)
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.log(error);
  });

The above command returns JSON structured like this:

{
  "cgp": [
    [0, 95],
    [15, 110],
    [30, 140],
    [45, 155],
    [60, 145],
    [75, 125],
    [90, 110],
    [105, 100],
    [120, 95],
    [135, 92]
  ],
  "scoring": "medium_impact",
  "cgp_min": 70,
  "cgp_max": 180
}

Returns a glucose response curve for a meal, covering the two hours and 15 minutes after startTime as 10 points at 15-minute intervals. The curve is computed from the user's profile and the foods in the request. Nothing is stored — the profile travels with every call.

You can optionally provide cgmData and consumedFoods to personalize the prediction with the user's own history. Like the profile, the history is used only to compute this one response — it is not stored, and there is no separate setup or training step to run first. When you provide history, it must include at least five complete training days — otherwise the request fails with 400 Bad Request and a message starting with "Not enough good days for AI Training".

A training day is complete when it meets both of the following criteria:

Days are the user's local calendar days in x-partner-timezone and don't need to be consecutive, but only full calendar days count — the partial first and last days of the history you send never qualify. Send a margin: seven calendar days of history is a safe minimum for five complete days.

Send at most one CGM reading per 15-minute window — downsample denser feeds (e.g. 5-minute CGM data) first. This is a hard requirement, not advice: denser data fails the request instead of being downsampled server-side.

HTTP Request

POST https://partners.january.ai/v1.1/cgm/glucose-predict

Headers

Header Required Description
Authorization Yes Bearer token for authentication
Content-Type Yes Must be application/json
x-partner-timezone Yes User's local IANA timezone (e.g. America/New_York). Defines the user's local calendar days when evaluating optional history; an invalid name fails the request

Request Body

Parameter Required Description
userProfile Yes User's profile data
userProfile.age Yes User's age
userProfile.gender Yes Exactly male or female, lowercase. Other values and casings (e.g. Male) are not rejected — the prediction silently ignores gender instead, so match the exact values
userProfile.height Yes User's height in inches
userProfile.weight Yes User's weight in pounds
userProfile.activity_level No Activity level ('sedentary', 'lightly_active', 'moderately_active', 'very_active'). Case-sensitive — Sedentary returns a 400
userProfile.medical_conditions No Array of medical conditions (excluding 'Type 1 diabetes'). Values are case-sensitive and must match the list below exactly
foods Yes Array of foods and servings for prediction. An empty array is not rejected but produces a meal-less baseline curve — always send at least one food
foods[].id Yes Unique identifier of the food item
foods[].serving Yes Serving details
foods[].serving.id Yes Serving size identifier
foods[].serving.quantity Yes Amount of serving consumed
startTime Yes ISO 8601 timestamp for when foods are consumed. Always include a timezone designator (Z or an offset) — a timestamp without one is read as UTC, not in x-partner-timezone
cgmData No CGM history used to personalize this prediction (in-request only; not stored) — at most one reading per 15-minute window. If cgmData is provided, consumedFoods must be provided also.
cgmData[].timestamp Yes ISO 8601 timestamp for the CGM data point
cgmData[].value Yes Blood sugar level (in mg/dL)
consumedFoods No Array of historical user foods and servings, paired with cgmData to personalize this prediction (in-request only; not stored).
consumedFoods[].timestamp Yes ISO 8601 timestamp for when the food is consumed
consumedFoods[].id Yes Unique identifier of the food item
consumedFoods[].serving Yes Serving details
consumedFoods[].serving.id Yes Serving size identifier
consumedFoods[].serving.quantity Yes Amount of serving consumed

Medical Conditions

Valid medical conditions include:

Response Structure

The response includes:

Reliability

What to do when a request fails, times out, or returns 429 — and which requests are safe to send again.

Timeouts

Most endpoints respond in well under a second. Photo Scan and Edit Photo Scan Results run an AI model and can take tens of seconds — set a client timeout of 60 seconds, and expect an occasional 504 when the vision model itself times out (treat it like any other 5xx).

When your own timeout fires, treat the outcome as unknown: the server may or may not have completed the work. What that means per endpoint is in the table below.

Retrying safely

Endpoint After a timeout or 5xx
All four search endpoints, Food Alternatives, Retrieve Food Logs Retry freely — read-only
Glucose Prediction Retry freely — stateless, and identical input returns an identical curve
Delete Food Log Retry freely — idempotent by design; deleting an already-deleted log also succeeds
Edit Photo Scan Results Retry freely — stores nothing
Photo Scan Retry with care: every attempt, including failed ones, counts against the per-user daily scan allowance. Use one or two bounded retries with backoff
Create Food Logs Not safe to retry blindly — a retry after an ambiguous timeout can write a duplicate log entry. On timeout, call Retrieve Food Logs for the date and check whether the log landed before re-creating it; remove accidental duplicates with Delete Food Log

The API has no idempotency-key mechanism — an Idempotency-Key header is ignored, so it cannot make Create Food Logs retry-safe.

Rate limits

Rate limits are configured per partner, per API family, as part of your agreement — there is no universal published number. Exceeding a limit returns 429 with the body {"message": "Too Many Requests"}.

Three things about the buckets are worth knowing:

Handling a 429: honor the Retry-After header when one is present; most partner-limit 429s don't carry one, so back off exponentially starting around a minute. Never auto-retry a per-day allowance — that 429 cannot succeed until the day's window resets.

Error format

Every error is JSON carrying the HTTP status and a single message field: {"message": "..."}. (A crash inside the API — rare — returns {"statusCode": 500, "message": "Internal Server Error"} instead.) Status meanings are listed under Errors. Treat 5xx responses and network-level failures as transient and retry them per the table above.

Errors

The January AI API uses conventional HTTP status codes:

Error Code Meaning
400 Bad Request -- Your request is invalid or malformed. Most often this means the x-partner-user-id header is missing on an endpoint that requires it — see Missing x-partner-user-id (400).
401 Unauthorized -- Your API key is invalid or missing.
403 Forbidden -- You don't have permission to access this resource.
404 Not Found -- The specified resource could not be found.
405 Method Not Allowed -- You tried to access a resource with an invalid HTTP method.
406 Not Acceptable -- You requested a format that isn't supported.
422 Unprocessable Entity -- The request was well-formed but contains semantic errors.
429 Too Many Requests -- You're sending requests too quickly. Slow down and retry.
500 Internal Server Error -- We had a problem with our server. Try again later.
503 Service Unavailable -- We're temporarily offline for maintenance. Please try again later.

Error Response Format

Error responses are formatted like this:

{
  "message": "The 'query' parameter is required for this endpoint."
}

All error responses follow the same format: an object with a single top-level message field.

Unhandled 500 Internal Server Error responses are the exception: they also include a statusCode field, and the message is always the generic Internal Server Error.

Common Error Scenarios

Authentication Errors

Request Errors

Server Errors

Missing x-partner-user-id (400)

A user-scoped endpoint called without the x-partner-user-id header returns:

{
  "message": "The x-partner-user-id header is required for this endpoint. It identifies the end user this request is performed on behalf of. Use a stable ID from your system. See https://docs.january.ai/#authentication"
}

A valid API key alone is not enough on endpoints that act on a person. If the x-partner-user-id header is missing or empty on one of those endpoints, the request fails with 400 Bad Request and the message string above.

This is not an API key problem. Your key was accepted — the request just didn't say which end user it was for. Add the header and retry:

x-partner-user-id: YOUR_PARTNER_USER_ID

See Authentication for what value to send, and each endpoint's Headers table for whether it requires the header.

Questions?

If anything in these docs is unclear — or the API returns something they don't explain — email umer@january.ai. Don't hesitate to reach out; we're happy to help.