curl --request POST \
--url https://app.skcal.fit/api/log/analyze \
--header 'Content-Type: multipart/form-data' \
--cookie __Secure-better-auth.session_token= \
--form photos=@treadmill.jpg \
--form 'note=today'\''s treadmill session' \
--form photos.items='@example-file'import requests
url = "https://app.skcal.fit/api/log/analyze"
files = { "photos.items": ("example-file", open("example-file", "rb")) }
payload = {
"photos": "@treadmill.jpg",
"note": "today's treadmill session"
}
headers = {"cookie": "__Secure-better-auth.session_token="}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('photos', '@treadmill.jpg');
form.append('note', 'today's treadmill session');
form.append('photos.items', '{
"fileName": "example-file"
}');
const options = {method: 'POST', headers: {cookie: '__Secure-better-auth.session_token='}};
options.body = form;
fetch('https://app.skcal.fit/api/log/analyze', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.skcal.fit/api/log/analyze",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos\"\r\n\r\n@treadmill.jpg\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"note\"\r\n\r\ntoday's treadmill session\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos.items\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n{\r\n \"fileName\": \"example-file\"\r\n}\r\n-----011000010111000001101001--",
CURLOPT_COOKIE => "__Secure-better-auth.session_token=",
CURLOPT_HTTPHEADER => [
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.skcal.fit/api/log/analyze"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos\"\r\n\r\n@treadmill.jpg\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"note\"\r\n\r\ntoday's treadmill session\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos.items\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n{\r\n \"fileName\": \"example-file\"\r\n}\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("cookie", "__Secure-better-auth.session_token=")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.skcal.fit/api/log/analyze")
.header("cookie", "__Secure-better-auth.session_token=")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos\"\r\n\r\n@treadmill.jpg\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"note\"\r\n\r\ntoday's treadmill session\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos.items\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n{\r\n \"fileName\": \"example-file\"\r\n}\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.skcal.fit/api/log/analyze")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["cookie"] = '__Secure-better-auth.session_token='
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos\"\r\n\r\n@treadmill.jpg\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"note\"\r\n\r\ntoday's treadmill session\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos.items\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n{\r\n \"fileName\": \"example-file\"\r\n}\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"kind": "workout",
"ok": true,
"mealId": "3f8b1c2a-5d6e-4f70-8a1b-2c3d4e5f6071",
"items": [
{
"name": "grilled chicken breast",
"kcal": 284,
"proteinG": 53.4
}
],
"totalKcal": 374,
"totalProteinG": 54,
"note": "assumed a single chicken breast",
"photoKeys": [
"nick@mintlify.com/2026-06-29/3f8b1c2a"
],
"workout": {
"id": "9c2d1e4f-7a8b-4c5d-9e0f-1a2b3c4d5e6f",
"source": "manual",
"activityType": "run",
"summary": "5k run + pullups",
"description": "ran 5k easy, then 3x10 pullups",
"startedAt": 1783100000000,
"durationS": 1680,
"movingDurationS": null,
"distanceM": 5000,
"elevationGainM": null,
"energyKcal": 320,
"avgHr": 148,
"maxHr": null,
"avgPowerW": null,
"avgCadence": null,
"exercises": [
{
"exercise": "pullups",
"sets": 3,
"reps": 10,
"weightLb": null,
"durationS": null,
"distanceM": null,
"notes": null
}
],
"createdAt": 1783100001000
},
"pounds": 158.2,
"weightKg": 71.8,
"bodyFatPct": 17.2,
"site": "waist",
"inches": 31.8,
"needSite": false
}{
"error": "weightKg must be 9-320 kg",
"detail": "upstream model returned 500"
}{
"error": "weightKg must be 9-320 kg",
"detail": "upstream model returned 500"
}{
"error": "weightKg must be 9-320 kg",
"detail": "upstream model returned 500"
}Log a meal or workout from photos
Uploads 1–5 photos, classifies what they show — food, workout evidence (machine console, app/watch summary, whiteboard), a scale readout, or a tape-measure/measuring-app reading — and logs the matching entry (meal, workout, weigh-in, or body measurement). Returns the classification in kind.
curl --request POST \
--url https://app.skcal.fit/api/log/analyze \
--header 'Content-Type: multipart/form-data' \
--cookie __Secure-better-auth.session_token= \
--form photos=@treadmill.jpg \
--form 'note=today'\''s treadmill session' \
--form photos.items='@example-file'import requests
url = "https://app.skcal.fit/api/log/analyze"
files = { "photos.items": ("example-file", open("example-file", "rb")) }
payload = {
"photos": "@treadmill.jpg",
"note": "today's treadmill session"
}
headers = {"cookie": "__Secure-better-auth.session_token="}
response = requests.post(url, data=payload, files=files, headers=headers)
print(response.text)const form = new FormData();
form.append('photos', '@treadmill.jpg');
form.append('note', 'today's treadmill session');
form.append('photos.items', '{
"fileName": "example-file"
}');
const options = {method: 'POST', headers: {cookie: '__Secure-better-auth.session_token='}};
options.body = form;
fetch('https://app.skcal.fit/api/log/analyze', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://app.skcal.fit/api/log/analyze",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos\"\r\n\r\n@treadmill.jpg\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"note\"\r\n\r\ntoday's treadmill session\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos.items\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n{\r\n \"fileName\": \"example-file\"\r\n}\r\n-----011000010111000001101001--",
CURLOPT_COOKIE => "__Secure-better-auth.session_token=",
CURLOPT_HTTPHEADER => [
"Content-Type: multipart/form-data"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://app.skcal.fit/api/log/analyze"
payload := strings.NewReader("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos\"\r\n\r\n@treadmill.jpg\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"note\"\r\n\r\ntoday's treadmill session\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos.items\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n{\r\n \"fileName\": \"example-file\"\r\n}\r\n-----011000010111000001101001--")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("cookie", "__Secure-better-auth.session_token=")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://app.skcal.fit/api/log/analyze")
.header("cookie", "__Secure-better-auth.session_token=")
.body("-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos\"\r\n\r\n@treadmill.jpg\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"note\"\r\n\r\ntoday's treadmill session\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos.items\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n{\r\n \"fileName\": \"example-file\"\r\n}\r\n-----011000010111000001101001--")
.asString();require 'uri'
require 'net/http'
url = URI("https://app.skcal.fit/api/log/analyze")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["cookie"] = '__Secure-better-auth.session_token='
request.body = "-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos\"\r\n\r\n@treadmill.jpg\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"note\"\r\n\r\ntoday's treadmill session\r\n-----011000010111000001101001\r\nContent-Disposition: form-data; name=\"photos.items\"; filename=\"example-file\"\r\nContent-Type: application/octet-stream\r\n\r\n{\r\n \"fileName\": \"example-file\"\r\n}\r\n-----011000010111000001101001--"
response = http.request(request)
puts response.read_body{
"kind": "workout",
"ok": true,
"mealId": "3f8b1c2a-5d6e-4f70-8a1b-2c3d4e5f6071",
"items": [
{
"name": "grilled chicken breast",
"kcal": 284,
"proteinG": 53.4
}
],
"totalKcal": 374,
"totalProteinG": 54,
"note": "assumed a single chicken breast",
"photoKeys": [
"nick@mintlify.com/2026-06-29/3f8b1c2a"
],
"workout": {
"id": "9c2d1e4f-7a8b-4c5d-9e0f-1a2b3c4d5e6f",
"source": "manual",
"activityType": "run",
"summary": "5k run + pullups",
"description": "ran 5k easy, then 3x10 pullups",
"startedAt": 1783100000000,
"durationS": 1680,
"movingDurationS": null,
"distanceM": 5000,
"elevationGainM": null,
"energyKcal": 320,
"avgHr": 148,
"maxHr": null,
"avgPowerW": null,
"avgCadence": null,
"exercises": [
{
"exercise": "pullups",
"sets": 3,
"reps": 10,
"weightLb": null,
"durationS": null,
"distanceM": null,
"notes": null
}
],
"createdAt": 1783100001000
},
"pounds": 158.2,
"weightKg": 71.8,
"bodyFatPct": 17.2,
"site": "waist",
"inches": 31.8,
"needSite": false
}{
"error": "weightKg must be 9-320 kg",
"detail": "upstream model returned 500"
}{
"error": "weightKg must be 9-320 kg",
"detail": "upstream model returned 500"
}{
"error": "weightKg must be 9-320 kg",
"detail": "upstream model returned 500"
}Authorizations
Better Auth session. Signing in (Google or email magic link) sets an HttpOnly session cookie that scopes every request to that account. The web app and CLI use this; agents use the OAuth 2.1 flow of the MCP server.
Query Parameters
Day to log against (YYYY-MM-DD). Defaults to today.
Timezone offset in minutes (as from getTimezoneOffset), used to anchor a dated workout.
Body
The photos (multipart field photos), plus an optional note caption for context.
Response
The photos were classified and, unless kind is other, logged.
Result of classify-and-log. kind says what the photos showed; food results carry the MealAnalysis fields, workout results carry workout, and other logs nothing.
What the photos were classified as.
food, workout, weight, measurement, other "workout"
True when something was logged.
true
Created meal id (food only).
"3f8b1c2a-5d6e-4f70-8a1b-2c3d4e5f6071"
Estimated food items (food only).
Show child attributes
Show child attributes
[
{
"name": "grilled chicken breast",
"kcal": 284,
"proteinG": 53.4
}
]
Summed calories (food only).
374
Summed protein grams (food only).
54
Model assumptions note (food only).
"assumed a single chicken breast"
Stored photo keys (food only).
["nick@mintlify.com/2026-06-29/3f8b1c2a"]
A logged workout, normalized to the standard fields of Apple Health, Garmin, and Strava; metric fields are null when the user didn't state them.
Show child attributes
Show child attributes
Logged body weight in pounds (weight only).
158.2
Logged body weight in kilograms (weight only).
71.8
Body-fat percentage when the scale showed one (weight only).
17.2
Body site the measurement was logged against (measurement only).
"waist"
Measured value in inches (measurement only).
31.8
True when a measurement was read but the body site could not be identified — nothing was logged; resubmit with a caption naming the site.
false