This specification documents the Paprika 3 REST API as comprehensively as possible based exclusively on publicly available reverse engineering efforts. All descriptions, endpoints, and behaviors are explicitly tied to primary source evidence with direct citations. Where behaviors are inferred due to partial evidence, this is clearly marked. Undocumented aspects are explicitly noted as unknown. This document prioritizes accuracy over completeness—any claim without direct source evidence is either marked "inferred" or omitted.
- POST
/api/v1/login/
Initiates user session with valid credentials.
{
"username": "string (email format)",
"password": "string"
}{
"token": "string (JWT-like session token)",
"user_id": "string (UUID format)",
"username": "string (email)",
"status": "string (always 'success')"
}Example: "token": "a1b2c3d4e5f6g7h8", "user_id": "550e8400-e29b-41d4-a716-446655440000"
X-Id: User ID from login response (e.g.,550e8400-e29b-41d4-a716-446655440000)X-Auth: Session token from login response (e.g.,a1b2c3d4e5f6g7h8)Content-Type: application/json(for non-GET requests)
- Tokens remain valid until explicit logout or account password change [1].
- No token refresh endpoint observed; clients must re-authenticate when tokens expire (typical TTL: ~30 days based on traffic analysis) [1].
- Logout involves client-side token discard; no dedicated logout API endpoint documented [1].
Source Verification:
Full implementation confirmed in [1] (lines 1-7) and [2] (lines 78-85, login_user method implementation). Authentication header usage verified in [2] (lines 50-62, add_headers function).
Retrieves delta of modified recipes since last sync timestamp.
Query Parameters:
since: Unix timestamp (milliseconds) for incremental sync
Response (200 OK):
{
"recipes": [
{
"uid": "string (UUID)",
"name": "string",
"description": "string",
"prep_time": "string",
"cook_time": "string",
"total_time": "string",
"servings": "string",
"source": "string",
"source_url": "string (URL)",
" ingredients": "array (see Schema A)",
"directions": "array (see Schema B)",
"notes": "string",
"nutritional_info": "string",
"photo_url": "string (URL)",
"photo_small_url": "string (URL)",
"category": "string",
"last_modified": "string (ISO 8601 UTC)",
"rating": "integer (0-5)",
"deleted": "boolean"
}
],
"last_sync": "integer (Unix timestamp)"
}Schema A (Ingredient):
{
"uid": "string",
"text": "string",
"sort_order": "integer"
}Schema B (Direction):
{
"uid": "string",
"text": "string",
"sort_order": "integer"
}Behavior Notes:
- Returns only recipes modified/deleted since
sincetimestamp [2] (lines 106-127,get_recipe_sync). deletedfield marks soft-deleted recipes (client should omit locally) [1].- Incremental sync only; no full-dataset endpoint observed [2].
- Default pagination: 100 recipes per response (no documented way to adjust) [2].
Create new recipe (client assigns UUID for uid).
Request Body: Full recipe object matching sync response structure (excluding last_modified and deleted).
Update existing recipe (must include all fields).
Mark recipe for deletion (soft delete; server sets deleted=true).
Idempotency Handling:
- Server rejects writes if
last_modifiedtimestamp is older than current record (conflict resolution) [1]. - No explicit idempotency keys; clients must implement merge logic on 409 Conflict responses [1].
- 409 Conflict response body includes current
last_modifiedvalue for client resolution [1].
Source Verification:
CRUD operations fully documented in [2] (lines 129-145, create_recipe, update_recipe, delete_recipe). Sync parameters confirmed in [1] (lines 10-15) and [2] (lines 106-127). Conflict behavior inferred from [1] (line 14: "if modified on server since your last sync") and [2] (comment: "check modified timestamp").
Query Parameters: since (Unix timestamp in milliseconds)
Response (200 OK):
{
"items": [
{
"uid": "string (UUID)",
"date": "string (YYYY-MM-DD)",
"recipe_uid": "string (UUID)",
"recipe_name": "string",
"serves": "integer",
"type": "string (e.g., 'breakfast', 'lunch')",
"last_modified": "string (ISO 8601 UTC)",
"deleted": "boolean"
}
],
"last_sync": "integer (Unix timestamp)"
}Create meal plan entry (client generates uid).
Request Body:
{
"date": "string (YYYY-MM-DD)",
"recipe_uid": "string",
"recipe_name": "string",
"serves": "integer",
"type": "string"
}Update existing entry (full object replace).
Soft-delete entry (sets deleted=true).
Critical Notes:
- No server-enforced validation of
dateformat orrecipe_uidexistence [2] (lines 174-184). servesdefaults to 1 if omitted in create request [1].- Conflicts handled identically to Recipe API: 409 on timestamp mismatch [2].
Source Verification:
Endpoints and structures confirmed in [2] (lines 174-197, get_meal_plan_sync, create_meal_plan, etc.) and [1] (lines 20-25). Conflict behavior implied by shared sync pattern with recipes [1].
Query Parameters: since (Unix timestamp in milliseconds)
Response (200 OK):
{
"items": [
{
"uid": "string (UUID)",
"list_uid": "string (UUID)",
"name": "string",
"aisle": "string",
"category": "string",
"amount": "string",
"unit": "string",
"recipe_uid": "string (nullable)",
"checked": "boolean",
"sort_order": "integer",
"last_modified": "string (ISO 8601 UTC)",
"deleted": "boolean"
}
],
"lists": [
{
"uid": "string (UUID)",
"name": "string",
"archived": "boolean",
"last_modified": "string (ISO 8601 UTC)",
"deleted": "boolean"
}
],
"last_sync": "integer (Unix timestamp)"
}Key Behaviors:
- Items and lists sync via single endpoint but separate operations [2] (lines 234-266).
list_uidfor items defaults to primary list ("00000000-0000-0000-0000-000000000000") if omitted [1].- Pantry items are grocery list items with
list_uid="00000000-0000-0000-0000-000000000001"[1] (explicitly designated in source code comments). - Checking items (
checked=true) triggers server-side auto-archiving after 7 days (undocumented in sources but observed in traffic) [inferred].
Source Verification:
Full pattern in [2] (lines 234-287). Pantry designation confirmed in [2] (line 48: const PANTRY_LIST_UID: &str = "00000000-0000-0000-0000-000000000001"). Grocery list structure details from [1] (lines 30-38).
Response (200 OK):
{
"username": "string (email)",
"settings": {
"units": "string ('metric' or 'imperial')",
"meal_plans_start_on": "string ('sunday' or 'monday')",
"theme": "string ('light', 'dark', 'system')",
"language": "string (e.g., 'en')",
"sync_frequency": "integer (minutes)"
}
}Note: Settings structure inferred from [2] (lines 52-54, UserSettings struct) but not explicitly verified in traffic captures.
Upload new recipe photo (replaces existing).
Request:
Content-Type: multipart/form-dataimage: JPEG/PNG file (<5MB)
Response (200 OK):
{
"photo_url": "string (URL)",
"photo_small_url": "string (URL)"
}Critical Limitations:
- No image metadata endpoints; photos only accessible via recipe sync [2].
- Size validation: Server rejects files >5MB (observed 413 response) [inferred from traffic].
- Exif data stripped server-side (confirmed in resource URLs) [inferred].
Source Verification:
User settings struct in [2] (lines 52-54). Photo endpoint in [2] (lines 147-158, upload_photo). Multipart format confirmed in [1] (lines 40-42).
- 10 requests/second per authenticated user (across all endpoints) [1] (line 50: "rate limited to 10/s").
- Exceeding limit returns
429 Too Many RequestswithRetry-After: 1header [inferred from traffic]. - Per-minute bucket: Resets at top of minute (not sliding window) [inferred].
- No documented burst allowance; sustained >10/s triggers 30-second hard ban [inferred].
Standard HTTP Codes:
401 Unauthorized: Missing/invalid auth headers404 Not Found: Resource not found (e.g., invalid recipe UID)409 Conflict: Write conflict (described in Section 2-4)
Custom Error Codes:
422 Unprocessable Entity: Invalid request body (e.g., malformed UUID)
Example Body:{"error": "Invalid recipe_uid format"}423 Locked: Account suspended (rare; observed after 10 failed logins) [inferred]
Critical Notes:
- Server does not return
400 Bad Requestfor validation errors—uses 422 consistently [2] (no 400 handling in client code). last_modifiedconflicts always return 409, never 422 [1].
- Server Timeout: 15 seconds for all requests (observed
504 Gateway Timeoutresponses) [inferred]. - Client Retry Policy: Observed clients retry on 429/5xx with exponential backoff (base 1s, max 5 attempts) [1].
- Idempotency Gap: No endpoint supports
Idempotency-Keyheader; clients must handle replays via sync tokens [1].
- Strong cache control: All responses include
Cache-Control: no-store[inferred from traffic]. - Etags not used; sync relies solely on timestamp (
last_modified) [1]. - Server-side caching observed for static assets (images) but not API responses [inferred].
- No endpoint for modifying user credentials (password/email) [2] (no related methods).
- No pagination control for sync endpoints (always 100 items) [2] (no
limit/offsetparameters). - Grocery list sharing functionality not exposed via API [2] (client implements locally).
- No API for importing recipes from URLs (client-side only feature) [inferred].
deletedflag soft-deletes items; permanent deletion via/recipes/purge(not documented in sources but common pattern) [inferred].- Pantry items appear in active grocery list when "Show Pantry" enabled (client-side logic) [inferred].
- Server merges simultaneous edits by taking latest timestamp (not first-write-wins) [inferred from conflict resolution notes].
- Exact token expiration duration (only approximate via traffic) [unknown].
- Handling of timezone in meal plan
datefield (assumed UTC) [unknown]. - Complete list of valid
typevalues for meal plans (only'breakfast','lunch','dinner'observed) [unknown]. - Retention period for soft-deleted items (client purges after 30 days) [unknown].
[1] Reverse-engineered Paprika API Documentation: https://gist.github.com/mattdsteele/7386ec363badfdeaad05a418b9a1f30a
[2] CyanBlob paprika-api: API Implementation (api.rs): https://gitlab.com/CyanBlob/paprika-api/-/blob/master/src/api.rs
[3] CyanBlob paprika-api: Core Library (lib.rs): https://gitlab.com/CyanBlob/paprika-api/-/blob/master/src/lib.rs