API reference
Read and write your recipes, collections, pantry, meal plan, grocery list and nutrition over REST. Needs Pro or Family; on a free account every request returns 403 PREMIUM_REQUIRED. See plans.
Quickstart
Create a read-only API key, then list your most recent recipes:
curl https://recipeclip.app/api/v1/recipes \ -H "Authorization: Bearer rcp_live_YOUR_KEY"The response is a JSON list of your own saved recipes. has_more tells you whether there's another page; see Pagination.
{ "recipes": [ { "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "title": "Miso-Glazed Salmon", "description": "Pan-seared salmon with a sticky miso-honey glaze.", "image_url": null, "servings": "4", "prep_time": "10 min", "cook_time": "15 min", "visibility": "private", "created_at": "2026-08-20T17:32:10.482000+00:00",Authentication
Send the key as a bearer token on every request.
Can't set a custom Authorization header? Send X-API-Key: rcp_live_YOUR_KEY instead. Both are accepted the same way.
What a key can access
A key acts as its account. Listing, searching, and What can I cook only ever return recipes the account owns, and writes (create, update, delete, import) only ever touch recipes the account owns. Getting a single recipe by id can also reach a recipe shared into the account's family, though there's no endpoint that lists those ids, so you'd need to already know one.
Key format
A key is 65 characters: the fixed prefix rcp_live_, a 12-character public key id, an underscore, and a 43-character secret. The key id is safe to log. It is how a key is identified in the developer console and in error messages. The secret is shown once, at creation, and cannot be recovered afterward.
Scopes
Every key has one scope, chosen when it's created.
readread_write| Scope | Can call |
|---|---|
read | GET /api/v1, GET /api/v1/recipes, GET /api/v1/recipes/{id}, GET /api/v1/collections, GET /api/v1/cook, GET /api/v1/pantry, GET /api/v1/meal-plan, GET /api/v1/grocery-list, GET /api/v1/nutrition. |
read_write | Everything read can, plus POST /api/v1/recipes, POST /api/v1/recipes/import, PATCH /api/v1/recipes/{id}, DELETE /api/v1/recipes/{id}, POST /api/v1/pantry, DELETE /api/v1/pantry, POST /api/v1/meal-plan, PATCH /api/v1/meal-plan/{id}, DELETE /api/v1/meal-plan/{id}, POST /api/v1/grocery-items, PATCH /api/v1/grocery-items, DELETE /api/v1/grocery-items. |
A read-only key calling a write-only route gets 403 INSUFFICIENT_SCOPE, never a 401. The key is valid, it just can't do this.
Keys are secrets
/api/v1 and /api/mcp send no CORS headers on purpose, so a browser cannot read their responses cross-origin even if a key leaked into client code.Errors
Every error is JSON: { error, code, ...extra }. Branch on the status and code, not the message text. The message can change; the code won't.
400INVALID_REQUEST/cook's ingredients).400PANTRY_EMPTYGET /cook?pantry=true on an empty pantry. Add items first, or pass ingredients.400INVALID_CURSORnext_cursor unmodified.400VALIDATION_FAILED401AUTH_MISSINGAuthorization or X-API-Key header at all.401AUTH_MALFORMEDrcp_live_<id>_<secret>.401AUTH_INVALID401AUTH_REVOKED403PREMIUM_REQUIREDupgrade_required: true and an upgrade_url.403INSUFFICIENT_SCOPEread_write scope.403FORBIDDEN404NOT_FOUND404NOT_IN_FAMILYplan=family was requested but the account is not in a Family.409DUPLICATE_RECIPEsource_url was already saved. Retry with allowDuplicate: true to save a second copy.429RATE_LIMITED/min) or per-account (300/min) request ceiling tripped.429AI_RATE_LIMITEDPOST /recipes/import only).502FETCH_FAILEDPOST /recipes/import, after every fetch path failed).500 / 502 / 503 / 504AI_*POST /recipes/import only). Every AI_* body includes retryable: true or false. Retry only when it's true.500INTERNAL503INTERNAL| Status | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | Bad request shape. Missing or non-JSON body, or a missing required query param (e.g. /cook's ingredients). |
400 | PANTRY_EMPTY | GET /cook?pantry=true on an empty pantry. Add items first, or pass ingredients. |
400 | INVALID_CURSOR | Malformed cursor. Pass back the previous page's next_cursor unmodified. |
400 | VALIDATION_FAILED | Body parsed as JSON but failed schema validation (e.g. an empty title). |
401 | AUTH_MISSING | No Authorization or X-API-Key header at all. |
401 | AUTH_MALFORMED | Credential present but not shaped like rcp_live_<id>_<secret>. |
401 | AUTH_INVALID | Well-shaped key, but the id is unknown or the secret is wrong. |
401 | AUTH_REVOKED | The key exists but was revoked. |
403 | PREMIUM_REQUIRED | The account is on the free plan. The REST API and MCP server are part of Pro and Family. The body also has upgrade_required: true and an upgrade_url. |
403 | INSUFFICIENT_SCOPE | A read-only key called a route that needs the read_write scope. |
403 | FORBIDDEN | The recipe is reachable only via family sharing. You can read it, not write it. |
404 | NOT_FOUND | Unknown id, or not yours. Always 404, never 403, so the two cases look the same from outside. |
404 | NOT_IN_FAMILY | plan=family was requested but the account is not in a Family. |
409 | DUPLICATE_RECIPE | This source_url was already saved. Retry with allowDuplicate: true to save a second copy. |
429 | RATE_LIMITED | Per-key (120/min) or per-account (300/min) request ceiling tripped. |
429 | AI_RATE_LIMITED | The account's AI import allowance is spent (POST /recipes/import only). |
502 | FETCH_FAILED | Couldn't fetch the source page (POST /recipes/import, after every fetch path failed). |
500 / 502 / 503 / 504 | AI_* | The model provider failed (POST /recipes/import only). Every AI_* body includes retryable: true or false. Retry only when it's true. |
500 | INTERNAL | Something went wrong on our end. Logged server-side; safe to retry. |
503 | INTERNAL | Couldn't verify the API key (a lookup failure, not a bad key). Safe to retry. |
Two more 429s guard the key lookup itself, not any one route: 600/minute on every lookup attempt from one address, valid or invalid (a shared-egress integration with several keys behind one IP can hit this one too), and a tighter 30/minute on failed attempts specifically. That one only trips with a bad key. Both carry RATE_LIMITED.
Rate limits
Two ceilings apply to every request: one per key, one per account (so minting more keys doesn't buy more throughput). The API is for Pro and Family accounts only, so these limits apply to paid plans; a free account gets a 403 on every request. The AI import endpoint also draws from its own allowance.
| Limiter | Limit | Notes |
|---|---|---|
| Per key | 120 / minute | Sliding window. One bucket per API key. |
| Per account | 300 / minute | Sliding window, across every key and MCP connection combined. |
| AI imports, Pro/Family | 100 / day | Resets daily. This endpoint has its own 100, separate from imports in the app and over MCP. |
A response from a valid key includes your remaining budget:
X-RateLimit-Limit: 120X-RateLimit-Remaining: 119X-RateLimit-Reset: 1791295785Cache-Control: no-storeA 429 adds a Retry-After header, in seconds.
Pagination
List endpoints are cursor-paginated, not offset-paginated. The page you're on can't shift under you, even if new recipes are being saved at the same time.
| Parameter | Shape | Notes |
|---|---|---|
| limit | 1–100, default 25 | Out-of-range or non-numeric silently clamps to the default or max, never errors. |
| cursor | opaque string | Pass back the previous page's next_cursor unmodified. A malformed one 400s with INVALID_CURSOR. |
Every page response has next_cursor (string or null) and has_more. Keep following next_cursor until it's null:
let cursor;const recipes = []; do { const url = new URL("https://recipeclip.app/api/v1/recipes"); url.searchParams.set("limit", "100"); if (cursor) url.searchParams.set("cursor", cursor); const res = await fetch(url, { headers: { Authorization: "Bearer rcp_live_YOUR_KEY" } }); const page = await res.json(); recipes.push(...page.recipes); cursor = page.next_cursor;} while (cursor);Response objects
Three shapes recur across endpoints. Every endpoint below links back here instead of repeating the field list.
Recipe
List or search recipes and What can I cook return the summary fields only. Get, Create, Update and Import from a URL return every field, including the detail only rows. Delete doesn't return a recipe at all, only { deleted, id }. See Delete a recipe below.
| Field | Type | Nullable | In | Description |
|---|---|---|---|---|
| id | string | no | summary | UUID. |
| title | string | no | summary | 1–500 characters. |
| description | string | yes | summary | |
| image_url | string | yes | summary | |
| servings | string | number | yes | summary | |
| prep_time | string | number | yes | summary | |
| cook_time | string | number | yes | summary | |
| visibility | string | no | summary | "private" or "public", set in the app. There's no way to change it through this API yet. |
| created_at | string | no | summary | ISO 8601. |
| updated_at | string | yes | summary | |
| url | string | no | summary | The recipe's page in the RecipeClip app. |
| notes | string | yes | detail only | Not on List or search. Only Get, Create, Update, Import. |
| source_type | string | yes | detail only | e.g. "manual", "url". |
| source_url | string | yes | detail only | |
| video_url | string | yes | detail only | |
| ingredients | object[] | no (can be empty) | detail only | Each item: name, quantity, unit, raw_text, all strings, never null. |
| steps | string[] | no (can be empty) | detail only | In order. |
Collection
| Field | Type | Nullable | Description |
|---|---|---|---|
| id | string | no | UUID. |
| name | string | no | |
| description | string | yes | |
| visibility | string | no | "private" or "public". |
| recipe_count | number | no | Recipes in the collection that aren't trashed. |
| created_at | string | no | ISO 8601. |
| url | string | no | Always https://recipeclip.app/collections. There's no per-collection page or recipe filter in this API yet. |
Ingredient match
What can I cook returns one of these per recipe: every Recipe (summary) field above, plus:
| Field | Type | Nullable | Description |
|---|---|---|---|
| match_percent | number | no | 0–100: share of the recipe's own ingredients you have, rounded. |
| matched_ingredients | string[] | no | Ingredient names from the recipe that you have. |
| missing_ingredients | string[] | no | Ingredient names from the recipe that you don't. |
Endpoints
Every path below is shown in full. Append it to https://recipeclip.app.
Check your key
/api/v1Read onlyNo parameters. The cheapest way to confirm a key works and see your account's limits and the full endpoint list.
Request
curl https://recipeclip.app/api/v1 \ -H "Authorization: Bearer rcp_live_YOUR_KEY"Response
{ "version": "v1", "account": { "user_id": "3f1c9a7e-2b4d-4a6e-9c8f-1a2b3c4d5e6f" }, "key": { "id": "AbC123dEf456", "scope": "read" }, "limits": { "requests_per_minute": 120, "default_page_size": 25,Errors5
401AUTH_MISSING401AUTH_MALFORMED401AUTH_INVALID401AUTH_REVOKED429RATE_LIMITED| Status | Code | Meaning |
|---|---|---|
401 | AUTH_MISSING | No credential. See Authentication. |
401 | AUTH_MALFORMED | Credential present but unparseable. |
401 | AUTH_INVALID | Unknown key id or wrong secret. |
401 | AUTH_REVOKED | The key was revoked. |
429 | RATE_LIMITED | Too many requests. |
Recipes
List or search recipes
/api/v1/recipesRead onlyYour own recipes, newest first, optionally filtered by a title search. Returns the Recipe (summary) fields; see Response objects.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| search | string (query) | no | Case-insensitive substring match against the title only. Stripped of , ( ) * % \ " ' and capped at 200 characters. |
| limit | integer (query) | no | 1–100. Default 25. |
| cursor | string (query) | no | Opaque. Pass back the previous page's next_cursor. |
Request
curl "https://recipeclip.app/api/v1/recipes?search=salmon&limit=10" \ -H "Authorization: Bearer rcp_live_YOUR_KEY"Response
{ "recipes": [ { "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "title": "Miso-Glazed Salmon", "description": "Pan-seared salmon with a sticky miso-honey glaze.", "image_url": null, "servings": "4", "prep_time": "10 min", "cook_time": "15 min", "visibility": "private", "created_at": "2026-08-20T17:32:10.482000+00:00",Errors2
400INVALID_CURSOR429RATE_LIMITED| Status | Code | Meaning |
|---|---|---|
400 | INVALID_CURSOR | Malformed cursor. |
429 | RATE_LIMITED | Too many requests. |
Create a recipe
/api/v1/recipesRead and writeCreate a recipe from JSON, for a page you already parsed yourself. To import directly from a URL, see Import from a URL below.
Parameters
| Field | Type | Required | Description |
|---|---|---|---|
| title | string | yes | Trimmed. 1–500 characters. |
| description | string | null | no | Up to 5000 characters. |
| servings | string | number | null | no | Up to 50 characters if a string. |
| prep_time | string | number | null | no | Up to 50 characters if a string. |
| cook_time | string | number | null | no | Up to 50 characters if a string. |
| source_type | string | null | no | Up to 50 characters. |
| source_url | string (http/https URL) | null | no | Up to 2000 characters. |
| source_book_title | string | null | no | Up to 500 characters. |
| source_page_number | string | number | null | no | Up to 20 characters if a string. |
| image_url | string (http/https URL, or a same-origin relative path) | null | no | Up to 2000 characters. |
| notes | string | null | no | Up to 10,000 characters. |
| video_url | string (http/https URL) | null | no | Up to 2000 characters. |
| step_photos | string[] | null | no | Up to 50 items, 2000 characters each. |
| ingredients | object[] | null | no | Up to 200 items. Each item: name (≤300), unit (≤50), quantity (string ≤50 or number), raw_text (≤500). raw is also accepted as an alias for raw_text. |
| steps | string[] | null | no | Up to 100 items, 5000 characters each. |
| allowDuplicate | boolean | no | Save a second copy even if this source_url was already saved. Default false. |
Request
curl -X POST https://recipeclip.app/api/v1/recipes \ -H "Authorization: Bearer rcp_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "title": "Miso-Glazed Salmon", "description": "Pan-seared salmon with a sticky miso-honey glaze.", "servings": "4", "prep_time": "10 min", "cook_time": "15 min", "ingredients": [ { "name": "salmon fillets", "quantity": "4", "raw_text": "4 salmon fillets" }, { "name": "white miso paste", "quantity": "2", "unit": "tbsp", "raw_text": "2 tbsp white miso paste" }, { "name": "honey", "quantity": "1", "unit": "tbsp", "raw_text": "1 tbsp honey" }, { "name": "soy sauce", "quantity": "1", "unit": "tbsp", "raw_text": "1 tbsp soy sauce" } ], "steps": [ "Whisk the miso, honey and soy sauce into a glaze.", "Brush the glaze over the salmon fillets.", "Broil for 8-10 minutes, until caramelized and cooked through." ]}'Response
{ "recipe": { "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "title": "Miso-Glazed Salmon", "description": "Pan-seared salmon with a sticky miso-honey glaze.", "image_url": null, "servings": "4", "prep_time": "10 min", "cook_time": "15 min", "visibility": "private", "created_at": "2026-08-20T17:32:10.482000+00:00", "updated_at": "2026-08-20T17:32:10.482000+00:00",Errors3
400VALIDATION_FAILEDVALIDATION_FAILED" }. An entirely missing field gives Zod's own message instead.403INSUFFICIENT_SCOPE409DUPLICATE_RECIPEsource_url already saved. Response includes existing: { id, title }.| Status | Code | Meaning |
|---|---|---|
400 | VALIDATION_FAILED | A field failed validation, e.g. an empty title: { "error": "title: Title is required", "code": "VALIDATION_FAILED" }. An entirely missing field gives Zod's own message instead. |
403 | INSUFFICIENT_SCOPE | Key is read-only. |
409 | DUPLICATE_RECIPE | source_url already saved. Response includes existing: { id, title }. |
Get a recipe
/api/v1/recipes/{id}Read onlyOne recipe, with its full ingredients (quantity, unit, raw text) and ordered steps.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string (path) | yes | Recipe id. |
Request
curl https://recipeclip.app/api/v1/recipes/a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d \ -H "Authorization: Bearer rcp_live_YOUR_KEY"Response
{ "recipe": { "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "title": "Miso-Glazed Salmon", "description": "Pan-seared salmon with a sticky miso-honey glaze, ready in under 30 minutes.", "image_url": null, "servings": "4", "prep_time": "10 min", "cook_time": "15 min", "visibility": "private", "created_at": "2026-08-20T17:32:10.482000+00:00", "updated_at": "2026-08-22T09:15:44.118000+00:00",Errors1
404NOT_FOUND| Status | Code | Meaning |
|---|---|---|
404 | NOT_FOUND | Unknown id, or not yours. A family-shared recipe is reachable here too. |
Update a recipe
/api/v1/recipes/{id}Read and writeUpdates title, description, servings, prep_time, cook_time, notes, image_url, ingredients and steps, all optional (field shapes: see Create a recipe). A field you omit is left untouched, so a title-only edit doesn't wipe ingredients or steps. When you DO pass ingredients or steps, they replace the whole list.
source_type, source_url, video_url, source_book_title, source_page_number and step_photos aren't updatable yet. They pass validation, so sending them doesn't error, but they're silently ignored, so don't rely on them changing.Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string (path) | yes | Recipe id. |
Request
curl -X PATCH https://recipeclip.app/api/v1/recipes/a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d \ -H "Authorization: Bearer rcp_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "description": "Pan-seared salmon with a sticky miso-honey glaze, ready in under 30 minutes.", "notes": "Even better the next day, cold over rice."}'Response
{ "recipe": { "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "title": "Miso-Glazed Salmon", "description": "Pan-seared salmon with a sticky miso-honey glaze, ready in under 30 minutes.", "image_url": null, "servings": "4", "prep_time": "10 min", "cook_time": "15 min", "visibility": "private", "created_at": "2026-08-20T17:32:10.482000+00:00", "updated_at": "2026-08-22T09:15:44.118000+00:00",Errors3
403INSUFFICIENT_SCOPE403FORBIDDEN404NOT_FOUND| Status | Code | Meaning |
|---|---|---|
403 | INSUFFICIENT_SCOPE | Key is read-only. |
403 | FORBIDDEN | Reachable only via family sharing. You don't own it. |
404 | NOT_FOUND | Unknown id, or not yours. |
Delete a recipe
/api/v1/recipes/{id}Read and writeMoves the recipe to trash, the same soft delete the app itself uses, so nothing is permanently removed.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string (path) | yes | Recipe id. |
Request
curl -X DELETE https://recipeclip.app/api/v1/recipes/a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d \ -H "Authorization: Bearer rcp_live_YOUR_KEY"Response
{ "deleted": true, "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d"}Errors1
404NOT_FOUND| Status | Code | Meaning |
|---|---|---|
404 | NOT_FOUND | Unknown id, not yours, or already deleted. |
Import from a URL
/api/v1/recipes/importRead and writeFetch a recipe page, extract it with AI, and save it: a food blog post, a recipe site, or a video link. Spends one of the account's AI imports from the daily Pro/Family allowance for this endpoint (see Rate limits).
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| url | string (body) | yes | 1–2000 characters, http(s) only. Localhost, private-network and cloud addresses are rejected. |
| allowDuplicate | boolean (body) | no | Save a second copy even if this source_url was already saved. |
Request
curl -X POST https://recipeclip.app/api/v1/recipes/import \ -H "Authorization: Bearer rcp_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "url": "https://www.bbcgoodfood.com/recipes/easy-pancakes"}'Response
{ "recipe": { "id": "7e2d9f4a-1c6b-4e8a-9d3f-5a7b2c4e6f8d", "title": "Easy pancakes", "description": "Learn how to make the perfect pancakes every time with our foolproof easy crêpe recipe – elaborate flip optional", "image_url": null, "servings": "12", "prep_time": "10 min", "cook_time": "20 min", "visibility": "private", "created_at": "2026-09-13T11:42:05.902000+00:00", "updated_at": "2026-09-13T11:42:05.902000+00:00",A fetch-failure example (502 Bad Gateway)
{ "error": "Could not fetch that page after every attempt.", "code": "FETCH_FAILED", "retryable": false}Errors5
400INVALID_REQUEST409DUPLICATE_RECIPEsource_url was already saved. The AI extraction already ran by this point, so this still spends one AI import. Retry with allowDuplicate: true only if you actually want a second copy.429AI_RATE_LIMITED502FETCH_FAILED500 / 502 / 503 / 504AI_*| Status | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | url isn't a valid or reachable http(s) URL. |
409 | DUPLICATE_RECIPE | This source_url was already saved. The AI extraction already ran by this point, so this still spends one AI import. Retry with allowDuplicate: true only if you actually want a second copy. |
429 | AI_RATE_LIMITED | The account's AI import allowance is spent. |
502 | FETCH_FAILED | Every fetch path (direct, proxy, archive) failed. |
500 / 502 / 503 / 504 | AI_* | The model provider failed. retryable in the body says whether to retry. |
Collections
List collections
/api/v1/collectionsRead onlyYour collections; see Response objects. Read-only in this API: there's no create, update or delete for collections, and no way to filter recipes by collection yet.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| limit | integer (query) | no | 1–100. Default 25. |
| cursor | string (query) | no | Opaque. Pass back the previous page's next_cursor. |
Request
curl https://recipeclip.app/api/v1/collections \ -H "Authorization: Bearer rcp_live_YOUR_KEY"Response
{ "collections": [ { "id": "3b1f9b4a-6e3b-4a0e-9b7a-2b7b6b9b2b11", "name": "Weeknight dinners", "description": null, "visibility": "private", "recipe_count": 12, "created_at": "2026-08-14T09:00:00.000000+00:00", "url": "https://recipeclip.app/collections" } ],Errors1
429RATE_LIMITED| Status | Code | Meaning |
|---|---|---|
429 | RATE_LIMITED | Too many requests. |
Cook
What can I cook
/api/v1/cookRead onlyRanks your own recipes by how much of each one you could already make: match_percent is the share of the recipe's own ingredients that you have, ties broken by fewest missing. Matching is substring-based both ways, so "chicken" matches "chicken thighs".
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| ingredients | string (query) | one of | Comma-separated, e.g. salmon,miso,honey. Required unless pantry=true. |
| pantry | boolean (query) | no | true ranks against your saved pantry (salt, pepper and water are assumed). Combine with ingredients to use both. |
| limit | integer (query) | no | Default 10, clamped 1–50. An invalid value silently falls back to 10. |
Request
curl "https://recipeclip.app/api/v1/cook?ingredients=salmon,miso,honey" \ -H "Authorization: Bearer rcp_live_YOUR_KEY" # or rank against your saved pantrycurl "https://recipeclip.app/api/v1/cook?pantry=true" \ -H "Authorization: Bearer rcp_live_YOUR_KEY"Response
{ "matches": [ { "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "title": "Miso-Glazed Salmon", "description": "Pan-seared salmon with a sticky miso-honey glaze.", "image_url": null, "servings": "4", "prep_time": "10 min", "cook_time": "15 min", "visibility": "private", "created_at": "2026-08-20T17:32:10.482000+00:00",Errors2
400INVALID_REQUESTpantry=true was passed.400PANTRY_EMPTYpantry=true, but your pantry is empty and no ingredients were passed.| Status | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | Neither ingredients nor pantry=true was passed. |
400 | PANTRY_EMPTY | pantry=true, but your pantry is empty and no ingredients were passed. |
Pantry
List your pantry
/api/v1/pantryRead onlyWhat you keep at home, alphabetically. Salt, pepper and water are always assumed, never stored, and listed in always_assumed.
Request
curl "https://recipeclip.app/api/v1/pantry" \ -H "Authorization: Bearer rcp_live_YOUR_KEY"Response
{ "items": [ { "id": "7c1e9a52-3b4d-4f08-9a6e-2d5b8c0f1a34", "name": "basmati rice", "added_at": "2026-10-01T18:04:11.220000+00:00" }, { "id": "e04b77d1-92a3-4c6f-b1d8-5a3f0c9e6b72", "name": "eggs", "added_at": "2026-10-03T09:41:52.907000+00:00" }Add to your pantry
/api/v1/pantryRead and writeAdds ingredients. Names are matched the way the app matches them, so “Tomatoes” and “tomato” are the same item. Anything already there comes back in already_had, and salt, pepper and water in assumed.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| names | string[] (body) | yes | 1–50 ingredient names, e.g. chicken thighs. |
Request
curl -X POST "https://recipeclip.app/api/v1/pantry" \ -H "Authorization: Bearer rcp_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "names": [ "chicken thighs", "eggs", "salt" ]}'Response
{ "added": [ { "id": "3f8a2c6d-1e07-4b95-8d43-6c7e9b0a5f18", "name": "chicken thighs", "added_at": "2026-10-06T12:15:03.551000+00:00" } ], "already_had": [ "eggs" ], "assumed": [Errors2
400INVALID_REQUEST403INSUFFICIENT_SCOPE| Status | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | names is missing, empty or over 50. |
403 | INSUFFICIENT_SCOPE | A read-only key. |
Remove from your pantry
/api/v1/pantryRead and writeRemoves items you've used up. Pass names or ids; anything that isn't in the pantry comes back in not_found.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| names | string (query) | one of | Comma-separated names, e.g. eggs,milk. |
| ids | string (query) | one of | Comma-separated pantry item ids from the list call. |
Request
curl -X DELETE "https://recipeclip.app/api/v1/pantry?names=eggs" \ -H "Authorization: Bearer rcp_live_YOUR_KEY"Response
{ "removed": [ { "id": "e04b77d1-92a3-4c6f-b1d8-5a3f0c9e6b72", "name": "eggs", "added_at": "2026-10-03T09:41:52.907000+00:00" } ], "not_found": []}Errors2
400INVALID_REQUEST403INSUFFICIENT_SCOPE| Status | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | Neither names nor ids was passed. |
403 | INSUFFICIENT_SCOPE | A read-only key. |
Meal plan
Read your meal plan
/api/v1/meal-planRead onlyPlanned meals for a date range, sorted by date and meal slot. A recipe is null when it's no longer available to this account.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| from | date (query) | no | First day, inclusive. Default today (UTC). |
| to | date (query) | no | Last day, inclusive. Default 6 days after from. |
| plan | string (query) | no | all (default), mine or family. |
Request
curl "https://recipeclip.app/api/v1/meal-plan?from=2026-10-06&to=2026-10-12" \ -H "Authorization: Bearer rcp_live_YOUR_KEY"Response
{ "from": "2026-10-06", "to": "2026-10-12", "plans": [ "mine", "family" ], "slots": [ { "key": "breakfast", "name": "Breakfast" },Errors2
400INVALID_REQUEST404NOT_IN_FAMILYplan=family was requested but the account is not in a Family.| Status | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | A bad date, a range over 63 days, or an unknown plan. |
404 | NOT_IN_FAMILY | plan=family was requested but the account is not in a Family. |
Plan a meal
/api/v1/meal-planRead and writePuts one of your saved recipes on a date and meal slot. Returns 201 with the new entry.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| recipe_id | string (body) | yes | The id of a recipe in this account. |
| date | date (body) | yes | YYYY-MM-DD. |
| meal | string (body) | no | A meal slot key such as breakfast, lunch or dinner. Default dinner. |
| plan | string (body) | no | mine (default) or family. |
Request
curl -X POST "https://recipeclip.app/api/v1/meal-plan" \ -H "Authorization: Bearer rcp_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "recipe_id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "date": "2026-10-09", "meal": "dinner", "plan": "mine"}'Response
{ "meal": { "id": "9d2f4b86-7a13-4e5c-a0b9-1c8d3e6f2a47", "plan": "mine", "date": "2026-10-09", "meal": "dinner", "meal_label": "Dinner", "recipe": { "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "title": "Miso-Glazed Salmon", "url": "https://recipeclip.app/recipe/a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } }}Errors4
400INVALID_REQUESTrecipe_id, a bad date, or a meal that isn't one of your slots.403INSUFFICIENT_SCOPE404NOT_FOUND404NOT_IN_FAMILYplan=family was requested but the account is not in a Family.| Status | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | A missing recipe_id, a bad date, or a meal that isn't one of your slots. |
403 | INSUFFICIENT_SCOPE | A read-only key. |
404 | NOT_FOUND | No such recipe in this account. |
404 | NOT_IN_FAMILY | plan=family was requested but the account is not in a Family. |
Move a planned meal
/api/v1/meal-plan/{id}Read and writeChanges a planned meal's date, meal slot, or both. It works on your own plan and, if you're in a Family, on the family plan.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| id | string (path) | yes | The planned meal's id. |
| date | date (body) | no | New date. |
| meal | string (body) | no | New meal slot key. Send date, meal or both. |
Request
curl -X PATCH "https://recipeclip.app/api/v1/meal-plan/9d2f4b86-7a13-4e5c-a0b9-1c8d3e6f2a47" \ -H "Authorization: Bearer rcp_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "date": "2026-10-10"}'Response
{ "meal": { "id": "9d2f4b86-7a13-4e5c-a0b9-1c8d3e6f2a47", "plan": "mine", "date": "2026-10-10", "meal": "dinner", "meal_label": "Dinner", "recipe": { "id": "a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d", "title": "Miso-Glazed Salmon", "url": "https://recipeclip.app/recipe/a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d" } }}Errors3
400INVALID_REQUEST403INSUFFICIENT_SCOPE404NOT_FOUND| Status | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | Neither date nor meal was sent, or a value is invalid. |
403 | INSUFFICIENT_SCOPE | A read-only key. |
404 | NOT_FOUND | No planned meal with that id. |
Remove a planned meal
/api/v1/meal-plan/{id}Read and writeTakes one meal off the plan. The recipe itself is not touched.
Request
curl -X DELETE "https://recipeclip.app/api/v1/meal-plan/9d2f4b86-7a13-4e5c-a0b9-1c8d3e6f2a47" \ -H "Authorization: Bearer rcp_live_YOUR_KEY"Response
{ "removed": { "id": "9d2f4b86-7a13-4e5c-a0b9-1c8d3e6f2a47", "plan": "mine" }}Errors2
403INSUFFICIENT_SCOPE404NOT_FOUND| Status | Code | Meaning |
|---|---|---|
403 | INSUFFICIENT_SCOPE | A read-only key. |
404 | NOT_FOUND | No planned meal with that id. |
Grocery list
Read your grocery list
/api/v1/grocery-listRead onlyTwo lists in one: from_meal_plan is the ingredients for the meals planned in the range, combined and grouped by category; added_items are the things you added by hand, newest first.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| from | date (query) | no | First day, inclusive. Default today (UTC). |
| to | date (query) | no | Last day, inclusive. Default 6 days after from. |
| plan | string (query) | no | mine (default) or family: which meal plan to build from. |
| include_checked | boolean (query) | no | true also returns added items already checked off. |
Request
curl "https://recipeclip.app/api/v1/grocery-list?from=2026-10-06&to=2026-10-12" \ -H "Authorization: Bearer rcp_live_YOUR_KEY"Response
{ "from": "2026-10-06", "to": "2026-10-12", "plan": "mine", "from_meal_plan": [ { "name": "salmon fillets", "amount": "4", "category": "Meat & Seafood", "for_recipes": [ "Miso-Glazed Salmon" ]Errors2
400INVALID_REQUEST404NOT_IN_FAMILYplan=family was requested but the account is not in a Family.| Status | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | A bad date, a range over 63 days, or an unknown plan. |
404 | NOT_IN_FAMILY | plan=family was requested but the account is not in a Family. |
Add grocery items
/api/v1/grocery-itemsRead and writeAdds items to the list. Returns 201 with the new items, including the ids you need to check them off or remove them.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| items | object[] (body) | yes | 1–50 items, each { name, quantity?, unit? }. |
Request
curl -X POST "https://recipeclip.app/api/v1/grocery-items" \ -H "Authorization: Bearer rcp_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "items": [ { "name": "lemons", "quantity": "2" } ]}'Response
{ "added": [ { "id": "b5e8d1a3-4c92-4f67-9e0a-7d2c6b3f8a15", "name": "lemons", "quantity": "2", "unit": "", "checked": false, "added_at": "2026-10-06T12:20:44.318000+00:00" } ]}Errors2
400INVALID_REQUEST403INSUFFICIENT_SCOPE| Status | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | items is empty, over 50, or an item has no name. |
403 | INSUFFICIENT_SCOPE | A read-only key. |
Check off grocery items
/api/v1/grocery-itemsRead and writeMarks added items as bought, or puts them back on the list. Only items you added by hand can be checked; plan ingredients can't.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| ids | string[] (body) | yes | Item ids from the list or add call. |
| checked | boolean (body) | yes | true = bought, false = back on the list. |
Request
curl -X PATCH "https://recipeclip.app/api/v1/grocery-items" \ -H "Authorization: Bearer rcp_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "ids": [ "b5e8d1a3-4c92-4f67-9e0a-7d2c6b3f8a15" ], "checked": true}'Response
{ "updated": [ { "id": "b5e8d1a3-4c92-4f67-9e0a-7d2c6b3f8a15", "name": "lemons", "quantity": "2", "unit": "", "checked": true, "added_at": "2026-10-06T12:20:44.318000+00:00" } ], "not_found": []}Errors2
400INVALID_REQUEST403INSUFFICIENT_SCOPE| Status | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | ids is empty or checked isn't a boolean. |
403 | INSUFFICIENT_SCOPE | A read-only key. |
Remove grocery items
/api/v1/grocery-itemsRead and writeDeletes added items. Ids that don't match one of your items come back in not_found.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| ids | string (query) | yes | Comma-separated item ids. |
Request
curl -X DELETE "https://recipeclip.app/api/v1/grocery-items?ids=b5e8d1a3-4c92-4f67-9e0a-7d2c6b3f8a15" \ -H "Authorization: Bearer rcp_live_YOUR_KEY"Response
{ "removed": [ "b5e8d1a3-4c92-4f67-9e0a-7d2c6b3f8a15" ], "not_found": []}Errors2
400INVALID_REQUEST403INSUFFICIENT_SCOPE| Status | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | ids is missing. |
403 | INSUFFICIENT_SCOPE | A read-only key. |
Nutrition
Read a day of nutrition
/api/v1/nutritionRead onlyOne day of your food diary: the entries, their totals, your daily goals and what's remaining. goals and remaining are null until you set goals. An entry's nutrition is null when it was logged without numbers.
Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| date | date (query) | no | YYYY-MM-DD. Default today (UTC). |
| utc_offset_minutes | integer (query) | no | Your UTC offset in minutes, ISO sign: -300 for UTC-05:00, 60 for UTC+01:00. -840 to 840. Default 0. |
Request
curl "https://recipeclip.app/api/v1/nutrition?date=2026-10-06&utc_offset_minutes=0" \ -H "Authorization: Bearer rcp_live_YOUR_KEY"Response
{ "date": "2026-10-06", "entries": [ { "id": "2a6c9e41-8d3b-4f70-b5a2-0e7d1c4f9b36", "name": "Greek yogurt with berries", "logged_at": "2026-10-06T13:05:00+00:00", "meal": "breakfast", "servings": 1, "nutrition": { "calories": 210, "protein_g": 17.5,Errors1
400INVALID_REQUESTutc_offset_minutes.| Status | Code | Meaning |
|---|---|---|
400 | INVALID_REQUEST | A bad date or utc_offset_minutes. |
MCP
The same account is also reachable over MCP (Model Context Protocol, the way an AI assistant calls an app's tools directly) at the URL below, using the same API key or a signed-in connection. For setup steps per assistant, real screenshots, and the tool reference, see AI assistants.