SproutlySquad.dev

API conventions

Draft v0.1. This page is the vocabulary every API on this platform speaks: the shared domain types, the unit conventions, the confidence and status values, the auth header and the versioning policy. It is one URL, always current, so a link to it stays correct as the draft moves. What changed, and when, is in the changelog at the foot of the page.

Everything here is a draft contract: a shape published as a proposal. No endpoint serves it today, and nothing on this page should be read as documentation for one. Individual request and response signatures belong to each API’s own page, written in the vocabulary below rather than restating it.

Shared types

Four types carry across every API in the set. An identifier is opaque and stable: it is a key, never a name to show a user and never something to parse.

/** Opaque, stable, safe to store. Not a slug, and not parseable. */
type FoodId = string;

/** Anything the platform states about a food, with its provenance attached. */
interface Measured<T> {
  value: T;
  confidence: Confidence;
  /** Where the number came from: a dataset id, never a free-text note. */
  source: string;
}

interface Food {
  id: FoodId;
  /** Display name in the requested locale, defaulting to English. */
  name: string;
  /** ISO 8601 instant. The record changed, not the food. */
  updatedAt: string;
  status: Status;
}

interface NutrientProfile {
  /** Per 100 g of the food as described, unless a portion is given. */
  basis: 'per100g' | 'perPortion';
  energyKcal: Measured<number>;
  proteinG: Measured<number>;
  fatG: Measured<number>;
  carbohydrateG: Measured<number>;
}

Measured<T> is the reason the platform can be asked where a number came from. A bare number on a nutrition field is a number nobody can audit, so it is not a shape any API here returns.

Units

The unit lives in the field name, never in a sibling field and never in the value. proteinG is grams, always, in every API. Nothing returns a string like "12 g" for a quantity, and nothing returns a unit field beside a value field: two fields that have to agree are two fields that eventually will not.

SuffixUnitExample
GgramsproteinG
MgmilligramscalciumMg
UgmicrogramsvitaminB12Ug
KcalkilocaloriesenergyKcal
KjkilojoulesenergyKj
MlmillilitresvolumeMl
KgCo2ekilograms CO2 equivalentfootprintKgCo2e
Pctpercentage, 0 to 100bioavailabilityPct
Ratiounitless, 0 to 1digestibilityRatio

Numbers are JSON numbers, not strings. Rounding is the caller’s: the platform returns the precision it holds, which is frequently more than a label needs.

Data confidence

Every measured value carries one of four levels. The enum is ordered, and the order is meaningful: a consumer can refuse anything below a threshold.

type Confidence =
  /** Measured in a laboratory, attributable to a named dataset. */
  | 'measured'
  /** Taken from a reference table for the same food, not this sample. */
  | 'reference'
  /** Calculated from measured components of the same food. */
  | 'derived'
  /** Inferred from a similar food. Directionally useful, not a claim. */
  | 'estimated';

There is deliberately no unknown level. A value the platform cannot place at one of these four is a value it does not return, because an unlabelled number is the one a consumer trusts by accident.

Status

Status describes the record, not the request. It is how a record is retired without breaking a caller that stored its identifier.

type Status =
  /** Current. The default, and the only status most records ever hold. */
  | 'active'
  /** Superseded but still served. `replacedBy` names the successor. */
  | 'deprecated'
  /** Withdrawn: served for identifier resolution, with no values attached. */
  | 'retired';

A record never disappears. An identifier that resolved once resolves forever, which is what makes it safe to store one in your own database.

Authentication

One scheme, one header, every API:

Authorization: Bearer sq_live_<key>

Keys are per-environment and carry their environment in the prefix: sq_live_ and sq_test_. There is no API key in a query string, because a query string is written to logs, referrer headers and browser history. There is no cookie session and no OAuth flow: these are machine-to-machine APIs, and a browser that holds one of these keys has already leaked it.

Rate limits and quotas are per key, and every response states the remaining allowance in RateLimit headers.

Versioning

The major version is a prefix on every API path (v1). It changes only for a break, and a break is narrowly defined: removing a field, renaming one, changing a field’s type, or narrowing an enum. Adding a field, adding an enum member the ordered set already accommodates, or widening an optional are all done in place, without a version bump, so an integration that ignores unknown fields keeps working.

A superseded major version is served for twelve months after its successor ships, and Sunset headers state the date from the day the successor exists.

Feedback

This draft is meant to be argued with while it is still cheap to change. Push back in GitHub Discussions, where the conversation is public and the decisions that come out of it end up in the changelog below.

Changelog

v0.1, 16 September 2026. First draft: shared types, unit suffixes, the confidence and status enums, the auth header and the versioning policy.