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:
- Search foods by name, barcode, or natural language ("milkshake with banana") across branded foods, groceries, and restaurant menus.
- Scan meals from a photo to detect the foods on the plate and their nutrients.
- Find restaurant dishes on local and national menus, by name and location.
- Log food — create, retrieve, and manage each user's food diary.
- Predict glucose response before the first bite, from a user's demographics, health profile, and activity — no CGM or sensors required.
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.
Quickstart
Scan a photo of a meal. Replace
YOUR_API_KEYwith 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)
print(response.json())
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);
});
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.
Two of this request's headers deserve a close look now:
Authorization: Bearer YOUR_API_KEY— identifies your organization.x-partner-user-id: test-7f3c1a— identifies the end user the scan is performed on behalf of.
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.
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:
- Stable — the same end user must get the same value on every call, forever. This is the key January stores their data under.
- Unique per user within your organization.
- Opaque — never PII. Do not send email addresses, phone numbers, names, dates of birth, or medical record numbers. Our partners are health companies; use your internal user ID, or an opaque random ID you map to the user on your side.
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:
- Food logs written by Create Food Logs and read back by Retrieve Food Logs
- Photo scan history and the corrections made through Edit Photo Scan Results
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') |
| upc | Optional | The UPC representing the barcode of a food item |
| limit | Optional | Maximum number of results to return. Default is 10, maximum 400. Name searches (general/branded) never return more than 40 items |
| offset | Optional | Number of results to skip. 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 |
NLP Food Search
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;
totalNutrientssums 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 |
Restaurant Search
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. Must be sent together with lon |
| lon | Optional | Longitude for proximity ranking. Must be sent together with lat |
| distance | Optional | Search radius in meters. Default is 16093 (~10 miles), maximum 17000 |
| limit | Optional | Maximum number of results to return. Default is 50, maximum 100 |
Response Structure
The response includes:
- items: Array of restaurants — or of menu items, when coordinates are sent and the query matches no restaurant. Restaurant entries always carry
type,id,name, andis_chain;distance,city,address1, andaddress2are added only when nearby locations are found for the coordinates you sent
Restaurant Menu Search
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 |
| lon | Yes | Longitude to search around |
| distance | Optional | Search radius in meters. Default is 16093 (~10 miles), maximum 17000 |
| limit | Optional | Maximum number of results to return. Default is 50, maximum 100 |
Response Structure
The response includes:
- items: Array of menu items with id, name, restaurant name, photo URL, nutrients and primary serving information
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
- JPG, JPEG, PNG, WEBP, or non-animated GIF
- No location or personal metadata
- Well lit and in focus
Response Structure
The response includes:
- mealName: Name of the meal identified in the image
- totalNutrients: Summary of total nutritional content
- detections: Array of detected food items with confidence scores
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 |
| userInput | Yes | Text representation of changes to make |
Response Structure
The response includes:
- mealName: Modified name of the meal
- totalNutrients: Summary of total nutritional content
- detections: Array of modified food items with confidence scores
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 |
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);
});
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 |
| foods[].id | Yes | Unique identifier of the food item |
| foods[].serving | Yes | Serving details of the food item |
| foods[].serving.id | Yes | Serving size identifier |
| foods[].serving.quantity | Yes | Amount of the serving consumed |
| timestampUtc | Optional | UTC timestamp when food was consumed (YYYY-MM-DDTHH:MM:SSZ). When omitted, the log is stamped with the current server time |
| name | Optional | Name of the meal |
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);
});
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 (YYYY-MM-DD format) |
| end | Yes | End date (YYYY-MM-DD format) |
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. |
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. When you do, the history 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:
- At least 12 hours of CGM data
- At least 2 logged meals in distinct 15-minute windows
Send at most one CGM reading per 15-minute window — downsample denser feeds (e.g. 5-minute CGM data) first.
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 local timezone (e.g. America/New_York or US/Pacific) |
Request Body
| Parameter | Required | Description |
|---|---|---|
| userProfile | Yes | User's profile data |
| userProfile.age | Yes | User's age |
| userProfile.gender | Yes | User's gender ('male' or 'female') |
| 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') |
| userProfile.medical_conditions | No | Array of medical conditions (excluding 'Type 1 diabetes') |
| foods | Yes | Array of foods and servings for prediction |
| 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 |
| cgmData | No | CGM data array for AI training — 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 for AI training. |
| 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:
- Type 2 diabetes
- Prediabetes
- None of the above
Response Structure
The response includes:
- cgp: Array of data points representing glucose response curve [minute, glucose_value]
- scoring: Qualitative impact scoring ('low_impact', 'medium_impact', 'high_impact')
- cgp_min: Lower bound of the target glucose range. Always 70 mg/dL.
- cgp_max: Upper bound of the target glucose range: 180 mg/dL when
medical_conditionsincludes 'Type 2 diabetes', otherwise 140.
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.
- message: Human-readable description of what went wrong
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
- 401 Unauthorized: Missing or invalid API key
- 403 Forbidden: Valid API key but insufficient permissions
Request Errors
- 400 Bad Request: Missing required parameters or invalid parameter values — most commonly a missing
x-partner-user-idheader - 422 Unprocessable Entity: Valid request format but business logic errors
- 429 Too Many Requests: Rate limiting exceeded
Server Errors
- 500 Internal Server Error: Unexpected server-side error
- 503 Service Unavailable: Temporary service outage
Missing x-partner-user-id (400)
A user-scoped endpoint called without the
x-partner-user-idheader 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.