Skip to content

Instantly share code, notes, and snippets.

@mattdsteele
Last active April 13, 2025 13:04
Show Gist options
  • Save mattdsteele/7386ec363badfdeaad05a418b9a1f30a to your computer and use it in GitHub Desktop.
Save mattdsteele/7386ec363badfdeaad05a418b9a1f30a to your computer and use it in GitHub Desktop.

Paprika doesn't have their API documented, so this is me reverse-engineering it from an Android device

Live Demo

https://knowing-grain.glitch.me/

Code: https://glitch.com/edit/#!/knowing-grain

Sync

HTTP BASIC auth (emoji shrug), and send a GET to:

Syncing a particular recipe:

API uses BASIC auth; whatever your cloud sync is.

Services do not have CORS headers, so you can't invoke them directly from a browser :(

Saving/Updating a recipe

Send a POST to: https://www.paprikaapp.com/api/v1/sync/recipe/{uid-of-recipe}/

With your recipe in a multipart/form-data, in the data param. Example:

{
  "uid": "ccb42915-5fe9-425d-98da-c1ffbe420159",
  "name": "Test",
  "directions": "Do",
  "servings": "",
  "rating": 0,
  "difficulty": "Easy",
  "ingredients": "Ddddjdjdd\nDjdjdjdd\nDjdhdhdhdndnee ",
  "notes": "",
  "created": "2018-03-26 09:00:02",
  "image_url": null,
  "on_favorites": 0,
  "cook_time": "",
  "prep_time": "",
  "source": "",
  "source_url": "",
  "photo_hash": null,
  "photo": null,
  "nutritional_info": "",
  "scale": null,
  "deleted": false,
  "categories": [
    "cbaca738-cdfb-4150-960d-e1b1ac4cdcc3"
  ],
  "hash": "162e5ad0134e9398b98057aea951304780d0396582238320c28b34a7c35f841e"
}

The data param should be gzip-encoded

"Save Recipe" function

Still figuring this out 🤷‍♂️

Appears to be a POST to https://www.paprikaapp.com/api/v1/sync/recipes/, no auth

Sends a multipart/form-data with three fields:

Returns a text-plain (actually JSON) with a structure like:

{
    "result": {
        "cook_time": "4 mins. to 8 mins.",
        "difficulty": "",
        "directions": "To make the mix: Grind the oats in a food processor until they're chopped fine, but not a
powder.\n\nPut the flour, oats, and all other dry ingredients into a mixer with a paddle. Mix on slow speed, and
drizzle the vegetable oil into the bowl slowly while the mixer is running.\n\nStore in an airtight container for up
to two weeks at room temperature, or indefinitely in the refrigerator or freezer.\n\nTo make pancakes: Whisk
together 1 cup of mix, 1 cup of buttermilk (or a combination of half plain yogurt and half milk; or 3/4 cup liquid
whey), and 1 large egg. Don't worry if it seems thin at first: the oats will soak up the milk, and the mix will
thicken a bit as it stands.\n\nLet the batter stand for at least 20 minutes before cooking.\n\nHeat a lightly
greased griddle to 350°F (if you've got a griddle with a temperature setting; if not, medium-hot will do).\n\nDrop
the batter onto it in 1/4-cupfuls (a jumbo cookie scoop works well here) to make a 4\" diameter pancake. If you
have English muffin rings, use them; they make a perfectly round, evenly thick pancake.\n\nWhen the edges look dry
and bubbles come to the surface without breaking (after about 2 minutes, if your griddle is the correct
temperature), turn the pancake over to finish cooking on the second side, which will take about 2 minutes.\n\nServe
pancakes immediately, or stack and hold in a warm oven.\n\nYield: a batch using 1 cup of the mix will make about 5
to 8 pancakes, depending on size.",
        "image_url": "",
        "ingredients": "MIX\n4 cups King Arthur White Whole Wheat Flour or Organic White Whole Wheat Flour\n1 cup
King Arthur Unbleached All-Purpose Flour or Organic All-Purpose Flour\n3 1/2 cups old-fashioned or rolled oats\n3
tablespoons sugar\n3 tablespoons baking powder\n1 tablespoon salt\n1 tablespoon baking soda\n1 cup vegetable
oil\nPANCAKES\n1 cup homemade mix\n1 cup buttermilk, nut milk, or a combination of plain yogurt and milk; or 3/4
cup liquid whey\n1 large egg",
        "name": "Homemade Whole-Grain Pancake Mix",
        "notes": "",
        "nutritional_info": "Calories: 110\nTotal Carbohydrates: 12g\nCholesterol: 30mg\nTotal Fat: 5g\nDietary
fiber: 3g\nProtein: 4g\nSaturated fat: 1g\nAmount Per: 1 pancake (56g)\nSodium: 260mg\nSugar: 3g\nTrans Fat: 0g",
        "prep_time": "20 mins.",
        "servings": "10 cups dry mix",
        "total_time": ""
    }
}

You can then convert it to a recipe by making a UID and POSTing it to the URLs above

@8bitgentleman
Copy link

8bitgentleman commented Sep 18, 2024

@anthonydibi Well there you go, that's pretty abusive of the API and I guarantee why you're being blocked. For context the official paprika app makes 2-4 API calls when loading and browsing with less than 1mb of data and 500 recipes.
You're doing around 150x the calls on build and then ~20x every hour continuously as well as a massive amount on page load. A single person browsing your site could visit the recipes page 3-4 a visit making well over 100 calls to the API, no clue how many visitors you have.

If paprika has started doing IP bans and rate limiting it's exactly because of abuse like this.

Regenerating every hour is beyond excessive, how many recipe changes are you making?? Even one a day feels like a lot. Do some reverse engineering of how the paprika app synced, haven't looked in a while but off the top of my head they do a small call to check if there are any changes in the db and then selectively pull in those changes.
That being said if you must reload that often you really should rely on your own cached data like how the official app does

@anthonydibi
Copy link

@anthonydibi Well there you go, that's pretty abusive of the API and I guarantee why you're being blocked. For context the official paprika app makes 2-4 API calls when loading and browsing with less than 1mb of data and 500 recipes. You're doing around 150x the calls on build and then ~20x every hour continuously as well as a massive amount on page load. A single person browsing your site could visit the recipes page 3-4 a visit making well over 100 calls to the API, no clue how many visitors you have.

If paprika has started doing IP bans and rate limiting it's exactly because of abuse like this.

Regenerating every hour is beyond excessive, how many recipe changes are you making?? Even one a day feels like a lot. Do some reverse engineering of how the paprika app synced, haven't looked in a while but off the top of my head they do a small call to check if there are any changes in the db and then selectively pull in those changes. That being said if you must reload that often you really should rely on your own cached data like the how the official app does

I don't have a setup for intercepting network calls from my iOS device unfortunately otherwise I would be curious to see how the fetching is setup on the iOS app as far as determining exactly which recipes have changed. I don't disagree that fetching on every page generation is excessive and at some point I'll probably leverage the /sync/status endpoint to store a counter in S3 and use that to determine whether any recipe changes have been made but right now I'm just playing around.

@8bitgentleman
Copy link

@anthonydibi Definitely don't want to discourage playing around, hacking together stuff around the web is super important and I've definitely considered integrating paprika into my website as well. All I'm saying is mindful of the services you're using. An IP ban from a small app like paprika (with free sync which is rare in 2024!) isn't just some artificial machine barrier it's another person trying to tell you something about your usage of their system.

@anthonydibi
Copy link

@anthonydibi Definitely don't want to discourage playing around, hacking together stuff around the web is super important and I've definitely considered integrating paprika into my website as well. All I'm saying is mindful of the services you're using. An IP ban from a small app like paprika (with free sync which is rare in 2024!) isn't just some artificial machine barrier it's another person trying to tell you something about your usage of their system.

Totally fair, I think it's good that they have a rate limit set up so that stupid web developers like me can't accidentally bring down the entire service 🤣 but yeah my goal is to eventually mimic the call patterns of the app so that ideally the recipes are always up-to-date on my site but only pull new data when needed

@oldfieldtc
Copy link

oldfieldtc commented Sep 19, 2024

@anthonydibi Did you get any errors when you were rate limited? I've just tried to authenticate with the curl example above and I'm now getting an 'Unrecognized client' error back. Wasn't doing this a couple of days ago for me so maybe they've now implemented a device check or something.

@anthonydibi
Copy link

@anthonydibi Did you get any errors when you were rate limited? I've just tried to authenticate with the curl example above and I'm now getting an 'Unrecognized client' error back. Wasn't doing this a couple of days ago for me so maybe they've now implemented a device check or something.

Nope I would just not be able to connect to the server. I’m seeing the same unrecognized client error now so they must be doing some sort of user agent check.

@jm-lamotte
Copy link

@anthonydibi Did you get any errors when you were rate limited? I've just tried to authenticate with the curl example above and I'm now getting an 'Unrecognized client' error back. Wasn't doing this a couple of days ago for me so maybe they've now implemented a device check or something.

Nope I would just not be able to connect to the server. I’m seeing the same unrecognized client error now so they must be doing some sort of user agent check.

I now am getting the same reply. I have a script that would check for updates in a DB and upload to Paprika. I ran it once tonight, and it was successful. I ran it a second time, and got the "Unrecognized client" error.
However, my postman sessions (postman.com) were still able to download recipes, with V1 or V2. I copied the V2 token I use in Postman to my script, instead of requesting a token by accessing V2/account/login, and the script runs fine.
So there seems to be something different in the way that the API handles V2 authentication and returning a token (I'm using python requests, but adding User-Agent didn't make any difference).
I'll dig into this in the following weeks.

@anthonydibi
Copy link

@anthonydibi Did you get any errors when you were rate limited? I've just tried to authenticate with the curl example above and I'm now getting an 'Unrecognized client' error back. Wasn't doing this a couple of days ago for me so maybe they've now implemented a device check or something.

Nope I would just not be able to connect to the server. I’m seeing the same unrecognized client error now so they must be doing some sort of user agent check.

I now am getting the same reply. I have a script that would check for updates in a DB and upload to Paprika. I ran it once tonight, and it was successful. I ran it a second time, and got the "Unrecognized client" error. However, my postman sessions (postman.com) were still able to download recipes, with V1 or V2. I copied the V2 token I use in Postman to my script, instead of requesting a token by accessing V2/account/login, and the script runs fine. So there seems to be something different in the way that the API handles V2 authentication and returning a token (I'm using python requests, but adding User-Agent didn't make any difference). I'll dig into this in the following weeks.

Gotcha so the auth endpoint must be gated by some sort of device check but authenticated endpoints do not have the same device check. That makes sense, there must be some device registration process then. I need to set up a process for intercepting requests from my iOS device as I’m itching to figure out what the new system is

@8bitgentleman
Copy link

Whatever changed the last (android) app update was in July so the app must have already been complying and now the API is just more strict

@jm-lamotte
Copy link

@anthonydibi Did you get any errors when you were rate limited? I've just tried to authenticate with the curl example above and I'm now getting an 'Unrecognized client' error back. Wasn't doing this a couple of days ago for me so maybe they've now implemented a device check or something.

Nope I would just not be able to connect to the server. I’m seeing the same unrecognized client error now so they must be doing some sort of user agent check.

I now am getting the same reply. I have a script that would check for updates in a DB and upload to Paprika. I ran it once tonight, and it was successful. I ran it a second time, and got the "Unrecognized client" error. However, my postman sessions (postman.com) were still able to download recipes, with V1 or V2. I copied the V2 token I use in Postman to my script, instead of requesting a token by accessing V2/account/login, and the script runs fine. So there seems to be something different in the way that the API handles V2 authentication and returning a token (I'm using python requests, but adding User-Agent didn't make any difference). I'll dig into this in the following weeks.

Gotcha so the auth endpoint must be gated by some sort of device check but authenticated endpoints do not have the same device check. That makes sense, there must be some device registration process then. I need to set up a process for intercepting requests from my iOS device as I’m itching to figure out what the new system is

If you want to intercept requests, I use Charles Proxy (https://www.charlesproxy.com - 30 days free trial).
I understand why my script worked once, then not anymore. It is based on V1 of Paprika API, except for one function, that fetches / creates categories. That only works with V2. So as I didn't need to create a category, my first run worked. The second attempt required category updates, and failed.
With this in mind, either I comment out the category function calls and stay with V1, or I use the existing V2 token as static, not requesting a new one when my script runs.
Hope this helps on the "Unrecognized client". It won't help on the rate limit.

@anthonydibi
Copy link

@anthonydibi Did you get any errors when you were rate limited? I've just tried to authenticate with the curl example above and I'm now getting an 'Unrecognized client' error back. Wasn't doing this a couple of days ago for me so maybe they've now implemented a device check or something.

Nope I would just not be able to connect to the server. I’m seeing the same unrecognized client error now so they must be doing some sort of user agent check.

I now am getting the same reply. I have a script that would check for updates in a DB and upload to Paprika. I ran it once tonight, and it was successful. I ran it a second time, and got the "Unrecognized client" error. However, my postman sessions (postman.com) were still able to download recipes, with V1 or V2. I copied the V2 token I use in Postman to my script, instead of requesting a token by accessing V2/account/login, and the script runs fine. So there seems to be something different in the way that the API handles V2 authentication and returning a token (I'm using python requests, but adding User-Agent didn't make any difference). I'll dig into this in the following weeks.

Gotcha so the auth endpoint must be gated by some sort of device check but authenticated endpoints do not have the same device check. That makes sense, there must be some device registration process then. I need to set up a process for intercepting requests from my iOS device as I’m itching to figure out what the new system is

If you want to intercept requests, I use Charles Proxy (https://www.charlesproxy.com - 30 days free trial). I understand why my script worked once, then not anymore. It is based on V1 of Paprika API, except for one function, that fetches / creates categories. That only works with V2. So as I didn't need to create a category, my first run worked. The second attempt required category updates, and failed. With this in mind, either I comment out the category function calls and stay with V1, or I use the existing V2 token as static, not requesting a new one when my script runs. Hope this helps on the "Unrecognized client". It won't help on the rate limit.

Have you tried just using a v1 token for v2 API calls? That worked for me (but I don't use any v2-specific endpoints so idk if those will require a v2 token, v2 endpoints may just redirect to v1)

@c0cl3c
Copy link

c0cl3c commented Apr 7, 2025

If anyone is still interested in integrating into Paprika, after hours of testing I found to obtain token I had to use v1 API, and V2 api to retrieve data.

For example,
curl -X POST https://paprikaapp.com/api/v2/account/login -d 'email=MY_EMAIL&password=MY_PAPRIKA_PASSWORD' with content-type application/x-www-form-urlencoded (Retrieve token)

Then

https://www.paprikaapp.com/api/v2/sync/groceries/ to get my grocery list which I integrate into my Task list.

@soggycactus
Copy link

Have you all been able to successfully POST a recipe to /v2/sync/recipes? I keep trying but only get 500 errors - it seems to only work when changing existing recipes, but I'm unable to create an entirely new recipe just from using the API

@jm-lamotte
Copy link

Have you all been able to successfully POST a recipe to /v2/sync/recipes? I keep trying but only get 500 errors - it seems to only work when changing existing recipes, but I'm unable to create an entirely new recipe just from using the API

Here's an extract of my code, that works. It uses V2 API and POST. I wrote this some time ago, but I think I remember you have to pass all fields to the API, even if they are empty.
Just update the username and password, and run it. You should get a recipe named "Recipe Name" in Papri

import json
import hashlib
import random
import uuid
import requests
import gzip

from requests.auth import HTTPBasicAuth
from datetime import datetime, timedelta
from dataclasses import asdict, dataclass, field, fields
from typing import List, Optional, Any, Dict

PAUSERNAME="<Your Paprika Username>"
PAPASSWORD="<Your Paprika Password"

basic_auth = HTTPBasicAuth(PAUSERNAME, PAPASSWORD)
PAPRIKA_AUTHENTICATION = {
    "email": PAUSERNAME,
    "password": PAPASSWORD
}

token_response = requests.post("https://www.paprikaapp.com/api/v1/account/login/", data=PAPRIKA_AUTHENTICATION, auth=basic_auth)
PAPRIKA_TOKEN = token_response.json()['result']['token']

UNKNOWN = Any  # Used for documenting unknown API responses

def sync_recipe_url(uid): return f'https://www.paprikaapp.com/api/v2/sync/recipe/{uid}/'

def generate_uuid():
    random_String = ''
    random_Str_Seq = "0123456789ABCDEF"

    uuid_Format = [8, 4, 4, 4, 12, 5, 16]
    for n in uuid_Format:
        for i in range(0, n):
            random_String += str(
                random_Str_Seq[random.randint(0, len(random_Str_Seq) - 1)])
        if n != uuid_Format[-1]:
            random_String += '-'
    return random_String

@dataclass
class PaprikaItem:

    def as_json(self): return json.dumps(self.as_dict())

    def as_dict(self):
        return asdict(self)

    def as_gzip(self) -> bytes:
        return gzip.compress(self.as_json().encode("utf-8"))

    def calculate_hash(self) -> str:
        fields = self.as_dict()
        fields.pop("hash", None)

        return hashlib.sha256(
            json.dumps(fields, sort_keys=True).encode("utf-8")
        ).hexdigest()

    def update_hash(self):
        self.hash = self.calculate_hash()

    def __repr__(self):
        return f"<{self}>"

@dataclass
class BaseRecipe(PaprikaItem):
    categories: List[str] = field(default_factory=list)
    cook_time: str = ""
    created: str = field(default_factory=lambda: str(datetime.utcnow())[0:19])
    description: str = ""
    difficulty: str = ""
    directions: str = ""
    hash: str = field(default_factory=lambda: hashlib.sha256(str(uuid.uuid4()).encode("utf-8")).hexdigest())
    image_url: str = ""
    ingredients: str = ""
    name: str = ""
    notes: str = ""
    nutritional_info: str = ""
    photo: str = ""
    photo_hash: str = ""
    photo_large: UNKNOWN = None
    prep_time: str = ""
    cook_time: str = ""
    rating: int = 0
    servings: str = ""
    source: str = ""
    source_url: str = ""
    total_time: str = ""
    uid: str = field(default_factory=lambda: generate_uuid().upper())
    in_trash: bool = False
    is_pinned: bool = False
    on_favorites: bool = False
    on_grocery_list: Optional[str] = None
    photo_url: Optional[str] = None
    scale: Optional[str] = None

    def upload_recipe(self):
        files: Dict = {}
        self.update_hash()
        files['data'] = self.as_gzip()
        result = requests.request("post", sync_recipe_url(self.uid), headers={'Authorization': f'Bearer {PAPRIKA_TOKEN}'}, files=files)
        return self.uid

Recipe = BaseRecipe(
    categories=[],
    notes="Notes",
    directions="Directions",
    ingredients="Ingredients",
    name="Recipe Name",
    servings="Servings",
    source="Source",
    source_url="URL",
    )

Recipe_Paprika_Uid = Recipe.upload_recipe()

exit(0)

@soggycactus
Copy link

@jm-lamotte You still grab the token from v1 though? Not v2?

@jm-lamotte
Copy link

@jm-lamotte You still grab the token from v1 though? Not v2?

Unfortunately, yes. The Token seems to be the same, though.
Any attempt to fetch the token using V2 fails with "Unrecognized client" (see previous comments). Using postman, I get the token from V1 URL, but not from V2 URL, using the exact same request.
I found another thread that reports the same issue: community.home-assistant.io/t/paprika-recipe-app-integration-whats-for-dinner-tonight/707405/14. It looks like something changed late September last year, as the following curl -X POST https://paprikaapp.com/api/v2/account/login --data-urlencode 'email=MY_EMAIL' --data-urlencode 'password=MY_PAPRIKA_PASSWORD' was working before, but not anymore.

Paprika App is still using V1 API, so until they switch to V2, we don't have a way of understanding what we are missing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment