Integrations

Your stack, five minutes to first food.

Copy-paste starters for the platforms people actually build nutrition apps in. Every snippet calls the same GET /search endpoint and parses the same bare-array response.

One rule for every stack: full-access keys stay on the server. If a key must ship in client-side code (browser, mobile bundle), create a read-only key with its own rate cap in the dashboard — it can only call GET data endpoints and can be rotated without touching your backend.

JavaScript (browser)

Use a read-only key, or proxy through your own backend so no key ships at all.

const url = new URL('https://api.getdietly.com/search');
url.search = new URLSearchParams({ q: 'greek yogurt', limit: '5' });

const response = await fetch(url, {
  headers: { Authorization: 'Bearer YOUR_READ_ONLY_KEY' }
});
if (!response.ok) throw new Error(`DietlyAPI ${response.status}`);
const foods = await response.json(); // bare array, [] on no match
console.log(foods[0]?.name, foods[0]?.calories_kcal, 'kcal/100g');

Node.js

Node 18+ has fetch built in. Read the rate-limit headers to pace batch jobs.

const response = await fetch(
  'https://api.getdietly.com/search?q=' + encodeURIComponent('greek yogurt') + '&limit=5',
  { headers: { Authorization: `Bearer ${process.env.DIETLY_API_KEY}` } }
);

if (response.status === 429) {
  const wait = Number(response.headers.get('Retry-After') || 1);
  await new Promise(r => setTimeout(r, wait * 1000)); // then retry
}
const foods = await response.json();

// pace long-running imports before you get throttled:
const remaining = Number(response.headers.get('X-RateLimit-Remaining'));
if (remaining < 5) {
  const reset = Number(response.headers.get('X-RateLimit-Reset') || 60);
  await new Promise(r => setTimeout(r, reset * 1000));
}

Next.js

A route handler keeps the key server-side; your React components call /api/food-search instead of DietlyAPI directly.

// app/api/food-search/route.js
export async function GET(request) {
  const q = new URL(request.url).searchParams.get('q') ?? '';
  if (q.length < 2) return Response.json([]);

  const upstream = await fetch(
    `https://api.getdietly.com/search?q=${encodeURIComponent(q)}&limit=8`,
    {
      headers: { Authorization: `Bearer ${process.env.DIETLY_API_KEY}` },
      next: { revalidate: 3600 }, // cache identical searches for an hour
    }
  );
  if (!upstream.ok) return Response.json([], { status: upstream.status });
  return Response.json(await upstream.json());
}

Python

import os
import requests

session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['DIETLY_API_KEY']}"

response = session.get(
    "https://api.getdietly.com/search",
    params={"q": "greek yogurt", "limit": 5},
    timeout=10,
)
response.raise_for_status()
foods = response.json()  # list[dict], not {'results': [...]}
for food in foods:
    print(food["name"], food.get("calories_kcal"), "kcal/100g")

React Native / Expo

Mobile bundles can be unpacked — ship a read-only key with a rate cap, never your full-access key. Images work without any key via image_thumb_url.

const searchFoods = async (query) => {
  const response = await fetch(
    `https://api.getdietly.com/search?q=${encodeURIComponent(query)}&limit=10`,
    { headers: { Authorization: 'Bearer YOUR_READ_ONLY_KEY' } }
  );
  if (!response.ok) return [];
  return response.json();
};

// barcode scan miss (404) is a normal state, not an error:
const scanBarcode = async (ean) => {
  const response = await fetch(`https://api.getdietly.com/barcode/${ean}`, {
    headers: { Authorization: 'Bearer YOUR_READ_ONLY_KEY' }
  });
  return response.status === 404 ? null : response.json();
};

Flutter

import 'dart:convert';
import 'package:http/http.dart' as http;

Future<List<dynamic>> searchFoods(String query) async {
  final uri = Uri.https('api.getdietly.com', '/search', {
    'q': query,
    'limit': '10',
  });
  final response = await http.get(uri, headers: {
    'Authorization': 'Bearer YOUR_READ_ONLY_KEY',
  });
  if (response.statusCode != 200) return [];
  return jsonDecode(response.body) as List<dynamic>; // bare array
}

PHP

$query = http_build_query(['q' => 'greek yogurt', 'limit' => 5]);
$context = stream_context_create(['http' => [
    'header' => 'Authorization: Bearer ' . getenv('DIETLY_API_KEY'),
    'timeout' => 10,
]]);

$body = file_get_contents("https://api.getdietly.com/search?$query", false, $context);
$foods = json_decode($body, true); // plain array, [] on no match
echo $foods[0]['name'] ?? 'No match';

AI assistants (LLM tool calling)

Give a model a search_foods tool and it can answer nutrition questions with real database rows instead of guesses. The definition below works with the Claude and OpenAI tool-calling formats alike.

{
  "name": "search_foods",
  "description": "Search the DietlyAPI food database (4M+ foods) by name or brand. Returns foods with calories, protein, fat, carbs and more per 100 g. Use for any question about the nutrition of a specific food or product.",
  "input_schema": {
    "type": "object",
    "properties": {
      "q": { "type": "string", "description": "Food or brand to search, e.g. 'greek yogurt'" },
      "limit": { "type": "integer", "description": "Max results, 1-25", "default": 5 }
    },
    "required": ["q"]
  }
}

When the model calls the tool, run the request server-side and return the JSON to the model:

async function runSearchFoods({ q, limit = 5 }) {
  const response = await fetch(
    `https://api.getdietly.com/search?q=${encodeURIComponent(q)}&limit=${limit}`,
    { headers: { Authorization: `Bearer ${process.env.DIETLY_API_KEY}` } }
  );
  return response.ok ? response.json() : [];
}

Next steps

The integration guide covers response shapes, nulls, retries, nutrition units and the launch checklist. The dashboard has a live playground and per-key usage. Where Open Food Facts data is displayed, include a visible credit such as Food data from Open Food Facts (ODbL) via DietlyAPI — details in the API Terms.