The Kallo API
Kallo has an HTTP API. It is the same API the web app and the mobile app use — there is no separate public one — and this page describes it honestly, including the parts that are not ready for you to build on.
The machine-readable description is at
https://kallo.fit/openapi.json. It is OpenAPI
3.1, and every operation has a unique operationId and a description. Where a
route validates its request body with a schema, the documented body is generated
from that same schema. A CI test also compares the spec with the route handlers,
so it fails if a documented body, success status or query parameter disagrees
with what the handler actually parses and returns.
Base URL
https://kallo.fit
Endpoints live under /api/v1/. The exceptions are /api/healthz, which is
unversioned because it is a liveness probe rather than part of the surface, and
two authenticated routes, POST /api/analyze-meal and
GET /api/og/macro-card/{shareId} (both described below).
Successful responses are 200 (the waitlist confirmation link is a 307
redirect). No endpoint answers 201, including the ones that create something.
Date-bucketed reads take a required tz query parameter: the offset in minutes
as Date.getTimezoneOffset() reports it, from -840 to 720. If it is missing,
you get a 400; the server never falls back to a stored timezone. The one
exception is the nutrition overview, where tz is optional and a missing value
buckets days in UTC.
Meal analysis and the macro card
POST /api/analyze-meal turns a sentence into an estimate. It answers with a
text/event-stream of server-sent events: stage progress, item_name and
item_macros as dishes are found, then result (or cheat_estimate). The
stream ends with analysis_complete, which carries an analysisId, or with
error. Nothing is saved until you pass that id to POST /api/v1/meals/confirm.
Authentication, body validation, plan and rate-limit checks all run before the
stream opens and return the ordinary JSON error below. Once the stream is open, a
failure can only arrive as an error event. The body is plain text made of
frames (event: <type>, then data: <json>); the OpenAPI component
AnalyzeMealStreamEvent describes one data payload.
GET /api/og/macro-card/{shareId} renders a shared meal as a PNG card. It needs
a session and applies the same visibility rule as reading the shared meal, so a
share you cannot see returns 404, the same as one that does not exist.
Authentication
Every endpoint except the ones tagged Public needs a Supabase-issued access
token for a Kallo user:
Authorization: Bearer <jwt>
The token carries no scopes. A bearer holds the account's full authority: it
can read every meal, change the profile, and delete the account. There is no way
to issue a narrower one. That is why
the protected-resource metadata
declares no scopes_supported — publishing a scope list nothing enforces would
be worse than publishing none.
Protected-resource metadata follows RFC 9728 and names the Supabase Auth issuer
as the authorization server. Kallo itself is not an authorization server: there
is no /oauth/authorize, no /oauth/token, and no dynamic client registration.
Errors
Every documented API error returns the same envelope:
{
"error": {
"code": "VALIDATION_FAILED",
"status": 400,
"retryable": false,
"message": "date must be YYYY-MM-DD.",
"resolution": "Correct the request using the published schema, then retry."
}
}
code is stable and meant to be branched on. message is for a human and may
be translated. retryable says whether repeating the identical request can
succeed. resolution gives an agent the next action without asking it to parse
the human prose. Redirect-only operations and the health probe document their
own response shapes in OpenAPI.
| Code | Status | Retryable | Means |
|---|---|---|---|
VALIDATION_FAILED | 400 | no | The request body or query is wrong, including a body that is not valid JSON. Fix it. |
NOT_AUTHENTICATED | 401 | no | Missing, expired or invalid token. |
feature_locked | 402 | no | The account's plan does not include this. |
NOT_FOUND | 404 | no | No such resource, or it is not yours. The two are deliberately indistinguishable. |
PROFILE_NOT_FOUND | 404 | no | The signed-in user has no profile yet. Sign in again or finish onboarding. |
BARCODE_NOT_FOUND | 404 | no | No product is known for this barcode. |
BARCODE_NOT_CACHED | 404 | no | The barcode was never looked up before being logged. Rescan it. |
OCR_INVALID_IMAGE | 400 | no | The image could not be read as a label photo. |
OCR_NO_LABEL_DETECTED | 422 | no | No nutrition table was found in the photo. |
CONFLICT | 409 | no | The resource changed under you, or a client-generated id you sent (for example a meal's mealId / newMealId) is already taken. Generate a new id. |
CIRCLE_LIMIT_REACHED | 409 | no | Someone else's group or friend limit is full. |
PAYLOAD_TOO_LARGE | 413 | no | The body is over the endpoint's cap (64 KB for most JSON endpoints). Send less. |
RATE_LIMITED | 429 | yes | Back off. Honour Retry-After when it is present. |
RATE_LIMITER_UNAVAILABLE | 503 | yes | The limiter could not decide on a paid route. Honour Retry-After. |
PIPELINE_TIMEOUT | 504 | yes | Meal analysis took too long. |
INTERNAL | 500 | yes | Our fault. |
Authentication is checked before the body is read. A request without a valid
token gets NOT_AUTHENTICATED, whatever its body looks like, so a 400 or 413
means the token was accepted. A collision on a client-generated id is always
the same CONFLICT, whether the id is yours or another account's; retrying the
same id will never succeed. If a save timed out and you are unsure whether it
landed, refetch the day before retrying with a fresh id.
Any /api/ path with no handler returns a 404 in this same JSON shape, never an
HTML page — so a mistyped endpoint is something your client can parse rather
than something it has to guess at.
Rate limits
There is no published per-endpoint quota. Limits exist and are applied per user
and, for the unauthenticated endpoints, per IP address. When you hit one you get
RATE_LIMITED with a Retry-After header where the wait is known. Treat that
header as authoritative rather than backing off on a fixed schedule.
What you may rely on
Operations tagged Public in the spec are safe to depend on:
GET /api/healthzPOST /api/v1/waitlistGET /api/v1/waitlist/confirmGET /api/v1/groups/invite/{slug}GET /api/v1/barcode/image/{code}
Everything else is marked x-internal: true. Those endpoints exist to serve
Kallo's own clients. They are versioned under /v1, but that version is not a
promise to third parties — a shape can change to suit the apps without a bump.
If you build on one, pin your expectations loosely and read the spec again
before you upgrade.
Cost
The free tier is real, needs no card, and is enough to use the API for anything you can do in the app on a free account. There is no separate API price and no sandbox environment — you develop against production, with your own account and your own data.
Related
- How an agent should use Kallo
- OpenAPI specification
- Your data — what is stored, and what reaches the model
- Contact