Skip to content

Commit f95a7a8

Browse files
authored
Merge pull request #1 from aarons22/claude/paprika-api-research-XmxZX
Add Paprika recipe API endpoint documentation
2 parents 97af460 + 46a5295 commit f95a7a8

File tree

1 file changed

+242
-4
lines changed

1 file changed

+242
-4
lines changed

API_REFERENCE.md

Lines changed: 242 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@ For high-level implementation patterns and sync logic, refer to `./CLAUDE.md`.
1212
- **V1 Authentication Required**: V2 can trigger "Unrecognized client" errors
1313
- **HTTP Basic Auth + Form Data**: Unique authentication pattern
1414
- **Gzip Response Handling**: Some responses compressed, some not
15-
- **No True Deletion**: Only soft delete via `purchased=True` supported
15+
- **No True Deletion**: Only soft delete via `purchased=True` (groceries) or `in_trash=True` (recipes)
1616
- **Client UUID Generation**: Must generate UUID4 (uppercase) for new items
17+
- **Two-Step Recipe Fetch**: List endpoint returns only `{uid, hash}` pairs; must fetch each recipe individually
18+
- **Hash-Based Change Detection**: Compare recipe hashes to detect changes without downloading full data
1719

1820
### Skylight Key Findings
1921
- **Individual Deletion Broken**: Standard REST DELETE /items/{id} non-functional
@@ -267,6 +269,238 @@ Authorization: Bearer <token>
267269

268270
---
269271

272+
### Recipes
273+
274+
The recipe API uses a two-step sync pattern: a lightweight list endpoint returns `{uid, hash}` pairs for change detection, and individual recipe details must be fetched one at a time.
275+
276+
**Sources:** [Matt Steele's Paprika API Gist](https://gist.github.com/mattdsteele/7386ec363badfdeaad05a418b9a1f30a), [paprika-recipes Python library](https://github.com/coddingtonbear/paprika-recipes), [paprika-rs Rust client](https://github.com/Syfaro/paprika-rs)
277+
278+
#### List All Recipes (Lightweight)
279+
**Request:**
280+
```http
281+
GET /v2/sync/recipes/
282+
Authorization: Bearer <token>
283+
```
284+
285+
**Response:**
286+
```json
287+
{
288+
"result": [
289+
{
290+
"uid": "07975578-DE1A-42AB-B184-6E8FCB9AB753",
291+
"hash": "a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890"
292+
},
293+
{
294+
"uid": "081835AE-B714-4A3D-97B3-81764AA96706",
295+
"hash": "f6e5d4c3b2a1098765fedcba0987654321fedcba0987654321fedcba09876543"
296+
}
297+
]
298+
}
299+
```
300+
301+
**Key Points:**
302+
- Returns ONLY `uid` and `hash` pairs — NOT full recipe data
303+
- Designed for efficient change detection: compare `hash` against cached values
304+
- Must fetch individual recipes via `/sync/recipe/{uid}/` for full details
305+
- V1 endpoint (`/v1/sync/recipes/`) also works with Basic Auth
306+
307+
#### Get Single Recipe (Full Details)
308+
**Request:**
309+
```http
310+
GET /v2/sync/recipe/{uid}/
311+
Authorization: Bearer <token>
312+
```
313+
314+
**Response:**
315+
```json
316+
{
317+
"result": {
318+
"uid": "07975578-DE1A-42AB-B184-6E8FCB9AB753",
319+
"name": "Jordan Marsh's Blueberry Muffins",
320+
"ingredients": "2 cups flour\n1/2 cup sugar\n2 tsp baking powder\n1/2 tsp salt\n1/3 cup butter\n1 egg\n1 cup milk\n1.5 cups blueberries",
321+
"directions": "1. Preheat oven to 375F.\n2. Mix dry ingredients.\n3. Cut in butter.\n4. Beat egg with milk, add to dry mix.\n5. Fold in blueberries.\n6. Fill muffin cups 2/3 full.\n7. Bake 25 minutes.",
322+
"description": "Classic blueberry muffin recipe from the Jordan Marsh department store",
323+
"notes": "Best with fresh blueberries. Can substitute frozen (don't thaw).",
324+
"nutritional_info": "",
325+
"servings": "12 muffins",
326+
"difficulty": "",
327+
"prep_time": "15 min",
328+
"cook_time": "25 min",
329+
"total_time": "40 min",
330+
"rating": 5,
331+
"categories": ["Breakfast", "Baking"],
332+
"source": "New York Times",
333+
"source_url": "https://cooking.nytimes.com/recipes/...",
334+
"image_url": "",
335+
"photo": "photo_filename.jpg",
336+
"photo_hash": "abc123def456...",
337+
"photo_large": null,
338+
"photo_url": "https://uploads.paprikaapp.com/...",
339+
"hash": "a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef1234567890",
340+
"created": "2024-06-15 10:30:00",
341+
"on_favorites": true,
342+
"on_grocery_list": null,
343+
"in_trash": false,
344+
"is_pinned": false,
345+
"scale": null
346+
}
347+
}
348+
```
349+
350+
**Note:** The V1 endpoint (`/v1/sync/recipe/{uid}/`) also works with Basic Auth.
351+
352+
#### Recipe Object Fields
353+
354+
| Field | Type | Default | Description |
355+
|---|---|---|---|
356+
| `uid` | `string` | UUID4 (uppercase) | Unique recipe identifier |
357+
| `name` | `string` | `""` | Recipe title |
358+
| `ingredients` | `string` | `""` | Newline-separated ingredient list |
359+
| `directions` | `string` | `""` | Step-by-step cooking instructions |
360+
| `description` | `string` | `""` | Recipe summary or notes |
361+
| `notes` | `string` | `""` | Additional notes |
362+
| `nutritional_info` | `string` | `""` | Nutritional data |
363+
| `servings` | `string` | `""` | Serving quantity (free text) |
364+
| `difficulty` | `string` | `""` | Recipe complexity level |
365+
| `prep_time` | `string` | `""` | Preparation duration |
366+
| `cook_time` | `string` | `""` | Cooking duration |
367+
| `total_time` | `string` | `""` | Total time |
368+
| `rating` | `int` | `0` | Star rating (0=unrated, 1-5) |
369+
| `categories` | `list[string]` | `[]` | Category names |
370+
| `source` | `string` | `""` | Attribution / source name |
371+
| `source_url` | `string` | `""` | Original recipe URL |
372+
| `image_url` | `string` | `""` | External image URL |
373+
| `photo` | `string` | `""` | Photo filename |
374+
| `photo_hash` | `string` | `""` | SHA256 hash of photo file |
375+
| `photo_large` | `string/null` | `null` | Large photo filename |
376+
| `photo_url` | `string/null` | `null` | Server-hosted photo URL (read-only) |
377+
| `hash` | `string` | SHA256(UUID4) | Change detection hash (64-char hex) |
378+
| `created` | `string` | Current datetime | Creation timestamp (`YYYY-MM-DD HH:MM:SS`) |
379+
| `on_favorites` | `bool` | `false` | Whether recipe is favorited |
380+
| `on_grocery_list` | `string/null` | `null` | Grocery list reference (if ingredients added) |
381+
| `in_trash` | `bool` | `false` | Soft deletion flag |
382+
| `is_pinned` | `bool` | `false` | Quick access marker |
383+
| `scale` | `string/null` | `null` | Serving size adjustment factor |
384+
385+
#### Create/Update Recipe
386+
**Request:**
387+
```http
388+
POST /v2/sync/recipe/{uid}/
389+
Authorization: Bearer <token>
390+
Content-Type: multipart/form-data
391+
392+
data: <gzip-compressed JSON recipe object>
393+
```
394+
395+
**Response:**
396+
```json
397+
{
398+
"result": true
399+
}
400+
```
401+
402+
**Critical Requirements:**
403+
1. Data must be gzip-compressed JSON of the **full recipe object** (not partial)
404+
2. Send as `multipart/form-data` with field name `data`
405+
3. Client must generate UUID4 (uppercase) for new recipes
406+
4. Must include ALL fields — empty strings for unused text fields, `false` for booleans, `0` for rating, `[]` for categories
407+
5. Update the `hash` field whenever recipe content changes (any 64-char hex string works)
408+
6. Do **NOT** use the bulk endpoint (`POST /v2/sync/recipes/`) for creating recipes — it returns 500 errors. Use the individual `/sync/recipe/{uid}/` endpoint instead
409+
410+
#### Delete Recipe (Soft Delete Only)
411+
**Method:** Set `in_trash: true` on the recipe object and POST the update.
412+
413+
```http
414+
POST /v2/sync/recipe/{uid}/
415+
Authorization: Bearer <token>
416+
Content-Type: multipart/form-data
417+
418+
data: <gzip-compressed JSON with in_trash=true>
419+
```
420+
421+
**Note:** No true DELETE endpoint exists for recipes.
422+
423+
#### Recommended Sync Workflow
424+
425+
1. **GET `/v2/sync/recipes/`** → get list of `{uid, hash}` pairs
426+
2. **Compare hashes** against locally cached values
427+
3. **GET `/v2/sync/recipe/{uid}/`** for each recipe with a changed or new hash
428+
4. **Cache** the full recipe data and hash locally for future comparison
429+
430+
This two-step approach minimizes bandwidth — most syncs only need the lightweight list, and full recipe data is only fetched when changes are detected.
431+
432+
---
433+
434+
### Sync Status
435+
436+
#### Get Sync Status
437+
**Request:**
438+
```http
439+
GET /v2/sync/status/
440+
Authorization: Bearer <token>
441+
```
442+
443+
**Response:**
444+
```json
445+
{
446+
"result": {
447+
"recipes": 42,
448+
"categories": 5,
449+
"meals": 12,
450+
"groceries": 8,
451+
"groceryaisles": 15,
452+
"groceryingredients": 30,
453+
"grocerylists": 3,
454+
"mealtypes": 4,
455+
"menuitems": 0,
456+
"menus": 0,
457+
"pantry": 10,
458+
"photos": 25,
459+
"bookmarks": 2
460+
}
461+
}
462+
```
463+
464+
**Key Points:**
465+
- Values are **change counters** that increment on modifications, not total counts
466+
- Useful for smart syncing: only fetch a resource type if its counter has changed since last check
467+
- Compare against previously stored values to detect which types need re-syncing
468+
469+
---
470+
471+
### Categories
472+
473+
#### List All Categories
474+
**Request:**
475+
```http
476+
GET /v2/sync/categories/
477+
Authorization: Bearer <token>
478+
```
479+
480+
**Response:**
481+
```json
482+
{
483+
"result": [
484+
{
485+
"uid": "CAT-UID-1",
486+
"name": "Breakfast",
487+
"order_flag": 0,
488+
"parent_uid": null
489+
},
490+
{
491+
"uid": "CAT-UID-2",
492+
"name": "Baking",
493+
"order_flag": 1,
494+
"parent_uid": null
495+
}
496+
]
497+
}
498+
```
499+
500+
**Note:** Recipe `categories` field contains category names (strings), not UIDs.
501+
502+
---
503+
270504
## Skylight Calendar API
271505

272506
### Base URL
@@ -617,13 +851,17 @@ Authorization: Token token="<base64_token>"
617851
1. **Gzip Compression**: Responses MAY be gzip compressed (check for `\x1f\x8b` magic bytes)
618852
2. **All-or-Nothing**: GET endpoints return ALL items from ALL lists/categories
619853
3. **Client-Generated IDs**: Must generate UUID4 (uppercase) for new items
620-
4. **Soft Delete Only**: True deletion not supported, use `purchased: true`
854+
4. **Soft Delete Only**: True deletion not supporteduse `purchased: true` for groceries, `in_trash: true` for recipes
621855
5. **Rate Limits**: Unknown - recommend 60+ second intervals
622856
6. **Multiple Lists**: Must filter items by `list_uid` client-side
623857
7. **Unofficial API**: May break with app updates, no official documentation
624858
8. **Token Expiration**: Unknown duration - handle 401 gracefully
625859
9. **Aisle Auto-Assignment**: Server assigns aisles based on `ingredient`/`name` - leave `aisle` field empty when creating
626860
10. **Preserve Aisles**: Don't overwrite aisle field when syncing - only sync `name`, `purchased`, timestamps
861+
11. **Two-Step Recipe Sync**: `/sync/recipes/` returns only `{uid, hash}` pairs; must fetch each recipe individually via `/sync/recipe/{uid}/`
862+
12. **No Bulk Recipe Create**: `POST /v2/sync/recipes/` (plural) returns 500 errors; use individual `/sync/recipe/{uid}/` endpoint
863+
13. **Recipe Hash**: Any 64-char hex string works; server does not strictly validate format. Update hash when content changes
864+
14. **Full Object Required**: Recipe POST/update requires ALL fields, not partial updates
627865

628866
### Skylight Specific
629867
1. **JSON:API Format**: All responses follow JSON:API specification
@@ -686,5 +924,5 @@ Authorization: Token token="<base64_token>"
686924

687925
---
688926

689-
*Last updated: 2026-01-31*
690-
*Based on reverse engineering and actual API responses*
927+
*Last updated: 2026-02-06*
928+
*Based on reverse engineering, actual API responses, and community implementations*

0 commit comments

Comments
 (0)