
Security News
GitHub Actions Checkout Now Blocks Risky pull_request_target Checkouts
GitHub Actions checkout now blocks risky pull_request_target checkouts by default to help prevent pwn request supply chain attacks.
@recode-software/cookidoo-api
Advanced tools
TypeScript client for the Cookidoo custom-recipes API (Thermomix). Auth, CRUD, typed MODE/INGREDIENT annotation builders, and a Polish step-text parser.
Unofficial TypeScript client for the Cookidoo custom recipes API
("Utworzone przepisy" / "Created recipes"). Build Thermomix-actionable
recipes programmatically: copy a public recipe into your account, then PATCH
it with structured MODE buttons and INGREDIENT chips that render natively
on TM7 / TM6.
This is a ground-up rewrite in TypeScript — not a port of
miaucl/cookidoo-api. It fixes the
recipeUrl bug in that library's add_custom_recipe_from (which points at
/recipes/recipe/{lang}/{id} and 404s on community-authored recipes) by
always using the documented cookidoo.{cc}/created-recipes/public/recipes/{lang}/{id}
form.
Status: unofficial, reverse-engineered. Vorwerk can change the API at any time. Use for your own account — don't hammer the service.
npm i @recode-software/cookidoo-api
Requires Node 20+ (uses global fetch).
import { CookidooClient, parseStep, mode, ingredient, step } from "@recode-software/cookidoo-api";
const client = new CookidooClient({
email: process.env.COOKIDOO_EMAIL!,
password: process.env.COOKIDOO_PASSWORD!,
country: "pl", // tmmobile subdomain
language: "pl", // URL path segment
});
// 1. Copy a public recipe into your account
const recipe = await client.recipes.copyFromPublic({
publicId: "01KB04WSJP4SHNBKJK4H4FT0PZ",
servingSize: 1,
});
// 2. PATCH meta (name, ingredients, yield, times)
await client.recipes.patchMeta(recipe.recipeId, {
name: "T1 D1 Śniadanie · Gofry cynamonowe",
ingredients: [
{ type: "INGREDIENT", text: "50 g jabłka" },
{ type: "INGREDIENT", text: "5 g masła ekstra" },
],
yield: { value: 1, unitText: "portion" },
prepTime: 1800,
totalTime: 1800,
});
// 3. PATCH instructions with MODE + INGREDIENT annotations.
// `parseStep` autodetects "praż", "Varoma", "obr. X", "wsteczne" …
const stepA = parseStep(
"Do naczynia włóż jabłko, praż 10 min/100°C/obr. 1. Przełóż.",
[{ display: "50 g jabłka", stem: "jabłk", amount: 50, unit: "g" }],
);
// Build a step manually if the parser doesn't recognize your phrasing
const stepB = step("Ubij 3 min/obr. 3,5.", [
mode.manual({ offset: 5, length: 14, time: 180, speed: "3.5" }),
]);
await client.recipes.patchInstructions(recipe.recipeId, [stepA, stepB]);
new CookidooClient(options)| Option | Type | Default |
|---|---|---|
email | string | — |
password | string | — |
country | string | — |
language | string | country |
baseUrl | string | https://{country}.tmmobile.vorwerk-digital.com |
fetch | typeof fetch | global fetch (lets you inject for tests) |
The client performs OAuth2 password grant lazily on first API call, caches
access_token + refresh_token, and refreshes once on a 401 response.
client.recipes| Method | Description |
|---|---|
list() | List your custom recipes |
get(id) | Fetch a recipe (full view, with annotations) |
copyFromPublic({publicId, servingSize?, retry?}) | Copy a public recipe into your account |
patchMeta(id, meta) | Update name / ingredients / yield / times |
patchInstructions(id, steps) | Replace instructions[] with new Step[] |
delete(id) | Delete a custom recipe |
Always do two PATCH calls (meta + instructions) — sending both in one
request triggers validationError. This matches what the web UI does.
@recode-software/cookidoo-api/builders)Type-safe constructors for MODE / INGREDIENT annotations. Each takes the
{offset, length} of the substring in the step text to pin the chip/button.
import { mode, ingredient, step } from "@recode-software/cookidoo-api/builders";
mode.manual({ offset, length, time: 20, speed: "5" }); // ⚠ unsupported on custom recipes
mode.browning({ offset, length, time: 600, temperature: 140 }); // 140|145|150|155|160
mode.blend({ offset, length, time: 30, speed: "7" }); // 6|6.5|7|7.5|8
mode.steaming({ offset, length, time: 900, speed: "1" }); // accessory: Varoma (no temperature field)
mode.warmUp({ offset, length, temperature: 70, speed: "1" }); // soft|1|2
mode.turbo({ offset, length, time: 2, pulseCount: 1 });
mode.dough({ offset, length, time: 120 });
mode.riceCooker({ offset, length });
ingredient.simple({ offset, length, description: "50 g jabłka" });
ingredient.structured({
offset, length,
description: "50 g jabłka",
amount: 50, unit: "g",
});
@recode-software/cookidoo-api/parser)Polish step-text parser (the only verified grammar). Detects:
N min/M°C/obr. X → MODE manual with temperatureN min/Varoma/obr. X → MODE steamingpraż N min/M°C/obr. X → MODE browning (temperature clamped to 140–160 °C)N min/obr. X → MODE manual (no temperature)wsteczne suffix → direction: "CCW"INGREDIENT with nested VOLUME annotationimport { parseStep } from "@recode-software/cookidoo-api/parser";
const s = parseStep("praż 10 min/100°C/obr. 1 jabłko", [
{ display: "50 g jabłka", stem: "jabłk", amount: 50, unit: "g" },
]);
Time conversion: s/sek → 1, min → 60, h → 3600. Ranges (4–6 minut)
collapse to the first number.
stem is a substring of the ingredient word that matches any inflected
form in the step text. Polish nouns decline ("jabłko", "jabłka", "jabłkiem"),
so the stem "jabłk" catches them all via \bjabłk\w*.
The parser is grammar-driven. PL (grammars.pl) is the only built-in and the
only grammar verified against live Cookidoo payloads. To parse step text from
another market (e.g. DE), supply a custom Grammar:
import { parseStep, type Grammar } from "@recode-software/cookidoo-api";
const grammarDe: Grammar = {
name: "de",
timeUnits: { s: 1, sek: 1, min: 60, h: 3600, std: 3600 },
timeUnitsPattern: "min|sek\\.?|std\\.?|s|h",
speedLabel: "Stufe",
reverseWord: "linkslauf|rückwärts",
browningTrigger: "anbr(a|ä)t",
};
parseStep("Rühren 20 s/60°C/Stufe 2.", [], { grammar: grammarDe });
If you verify a grammar against a real account in your market, PRs welcome.
Only POST /created-recipes/{lang} (copy-from-public) is throttled. After
~6–8 successful POSTs in a minute the endpoint returns 429 Too Many Requests
with code: "importFailed". There's no Retry-After header.
The library exposes CookidooRateLimitError and copyFromPublic has
built-in backoff enabled by default (30 → 60 → 90 → 120 seconds):
// Default backoff
await client.recipes.copyFromPublic({ publicId });
// Custom delays + progress callback
await client.recipes.copyFromPublic({
publicId,
retry: {
delaysMs: [30_000, 60_000, 90_000],
onRateLimit: ({ attempt, delayMs }) =>
console.log(`rate limited, waiting ${delayMs}ms (attempt ${attempt})`),
},
});
// Opt out
await client.recipes.copyFromPublic({ publicId, retry: false });
PATCH /created-recipes/{lang}/{id} is not rate-limited — you can hammer it.
name: "manual" is silently unsupported on custom recipes. The API
saves it, but the Cookidoo web/TM7 UI renders the chip struck-through with
unsupported=true. parseStep / findModeAnnotations therefore skip
manual-producing patterns by default (use { emitManual: true } only if
you're generating data for a non-custom-recipe context). For step text
like 20 s/obr. 5 that has no specific Tryb, leave it as plain text —
users will type the values on the machine.steaming has NO temperature field. Only time, speed,
direction, and accessory. Sending temperature returns
400 validationError. The builder and parser both reflect this.POST /organize/{lang}/api/custom-list/{id}
returns a fake 200 OK when you try to add a custom recipe — the list
stays empty. Use name prefixing (T1 D1 Śniadanie · …) or attach to the
calendar via POST /planning/{lang}/api/my-day instead.tools is read-only. The server populates it based on which TM
generations support your step modes.totalTime/prepTime are seconds on PATCH. The Schema.org GET view
may return them as ISO-8601 durations ("PT30M").import {
CookidooError,
CookidooAuthError,
CookidooRateLimitError,
} from "@recode-software/cookidoo-api";
try {
await client.recipes.copyFromPublic({ publicId });
} catch (err) {
if (err instanceof CookidooRateLimitError) {
// 429 — copy endpoint throttled
} else if (err instanceof CookidooAuthError) {
// 401 on token endpoint
} else if (err instanceof CookidooError) {
console.error(err.status, err.body);
}
}
npm install
npm run typecheck
npm test # unit tests, no network
npm run test:integration # real API — requires .env (see .env.example)
npm run build
Integration tests read credentials from .env (or ../.env). They create
recipes with a __test__ prefix and delete them in afterEach; an
afterAll sweep removes any leftovers. Required variables:
COOKIDOO_EMAIL=
COOKIDOO_PASSWORD=
COOKIDOO_COUNTRY=pl
COOKIDOO_LANGUAGE=pl
COOKIDOO_TEST_PUBLIC_RECIPE_ID=01KB04WSJP4SHNBKJK4H4FT0PZ # optional
Thanks to miaucl/cookidoo-api (Python) for paving the way.
MIT
FAQs
TypeScript client for the Cookidoo custom-recipes API (Thermomix). Auth, CRUD, typed MODE/INGREDIENT annotation builders, and a Polish step-text parser.
We found that @recode-software/cookidoo-api demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
GitHub Actions checkout now blocks risky pull_request_target checkouts by default to help prevent pwn request supply chain attacks.

Product
Socket now supports Custom Roles and Repository Access Permissions so organizations can control who can access specific repositories and actions.

Product
Socket MCP now lets AI assistants review org alerts, investigate threats using the Socket threat feed, and inspect package files in addition to dependency scoring.