.. _endpoints: How the (real) Duolingo API works: Endpoints ============================================ The Duolingo API is not officially documented, but can be found by sniffing the traffic from their mobile apps. The API is used for user information, Skills and their Lessons, etc. The base URL of the API is ``https://www.duolingo.com/api/1``. ``GET /version_info`` --------------------- **No authentication required** Response: .. code-block:: javascript { "dict_base_url": "http://d2.duolingo.com/", "feature_flags": { "disable_discussions": false, "duolingo_for_schools": true, "use_newrelic_on_ios": true }, "js_version": "//d7mj4aqfscim2.cloudfront.net/proxy/js2/2c5d8cfb60c72069af01-vendor.js-//d7mj4aqfscim2.cloudfront.net/proxy/js2/2c5d8cfb60c72069af01-duolingo.js", "logged_out_ab_options": { "course_button_experiment2": true, "course_title_experiment": true, "dummy_loggedout_ab_experiment": true, "login_modal_experiment": false, "signup_modal_buttons_experiment": true, "web_loggedout_no_background_image_experiment": true, "web_register_language_select_experiment": "scaled", "web_schools_splash_experiment": false }, "offline": { "enabled": true, "max_lessons": 5 }, "speech_host": "speech.duolingo.com", "supported_directions": { "en": [ "de", "es", "fr", "it", "pt" ], "es": [ "en" ], "fr": [ "en" ], "it": [ "en" ], "pt": [ "en" ] }, "tts_base_url": "https://d7mj4aqfscim2.cloudfront.net/", "tts_cdn_url": "http://static.duolingo.com/", "tts_voice_configuration": { "multi_voices": "{\"dn\": [\"dn\"], \"fr\": [\"fr\", \"fr/mathieu\"], \"en\": [\"en/salli\"], \"pt\": [\"pt\"], \"nb\": [\"nb/liv\"], \"de\": [\"de\"], \"tr\": [\"tr/filiz\"], \"it\": [\"it/carla\"], \"da\": [\"da\"], \"sv\": [\"sv/astrid\"], \"es\": [\"es\"]}", "path": "tts/{voice}/{type}/{id}", "voices": "{\"nb\": \"nb/liv\", \"en\": \"en/salli\", \"tr\": \"tr/filiz\", \"it\": \"it/carla\", \"sv\": \"sv/astrid\"}" } } Things of note: * ``tts_cdn_url`` and ``tts_voice_configuration``: These give the paths used for getting the audio files for various exercises. More on these later. * ``dict_base_url``: This is the base URL for the multi-language dictionary. ``GET /users/show?id={user_id}`` or /users/show?username={username} ------------------------------------------------------------------- With the ``users`` API, it's important to know Duolingo's concept of the current language. Instead of always returning all data about a language, Duolingo only returns the data on the language the user is currently learning, be it German, French, etc. This state is kept across web, mobile, etc. To get information on a user's progress in another language, it's necessary to change language TO that language first. **Content varies with authentication** Unauthenticated ``````````````` .. code-block:: javascript { "admin": false, "avatar": "https://duolingo-images.s3.amazonaws.com/avatars/1/AX2PP_c3ls", "bio": "", "blockers": [], "blocking": [], "browser_language": "en", "calendar": [], "certificates": [], "change-design": false, "cohort": null, "created": "\n\n\n\n\n\n\n\n\n3 years ago\n", "daily_goal": null, "dashboard_redesign": true, "deactivated": false, "delete-permissions": false, "events": [], "facebook_id": null, "followers": [], "following": [], "freeze-permissions": false, "fullname": "", "gplus_id": null, "has_google_now": false, "has_observer": false, "id": 1, "inventory": { "timed_practice": "grandfathered" }, "is_blocked_by": false, "is_blocking": false, "is_follower_by": false, "is_following": false, "is_self_observer": false, "language_data": { "es": { "all_time_rank": [ "1" ], "bonus_rows": [ 4 ], "bonus_skills": [ { "achievements": [], "beginner": false, "bonus": true, "category": "bonus", "changed": false, "comment_data": {}, "coords_x": 2, "coords_y": 1000, "dependencies": [], "dependencies_name": [], "description": "", "disabled": false, "explanation": null, "has_explanation": "", "icon_color": "purple", "id": "b1be4ade8dfba595f69342e4e11891f9", "index": "64", "known_lexemes": [], "language": "es", "language_string": "Spanish", "learned": false, "learning_threshold": 0, "learning_threshold_percentage": 0, "left_lessons": 0, "lesson": false, "lesson_number": 1, "locked": true, "mastered": false, "missing_lessons": 3, "more_lessons": 0, "name": "FLIRTING", "new_index": 64, "num_lessons": 3, "num_lexemes": 17, "num_missing": 17, "num_translation_nodes": 0, "path": [], "practice_recommended": false, "progress_percent": 0.0, "short": "Flirting", "strength": 0.0, "test": false, "test_count": 3, "title": "Flirting", "url_title": "Flirting", "words": [ "quieres", "eres", "en", "por", "n\u00famero", "cielo", "gustas", "ojos", "una", "hola" ] }, { "achievements": [], "beginner": false, "bonus": true, "category": "bonus", "changed": false, "comment_data": {}, "coords_x": 3, "coords_y": 1000, "dependencies": [], "dependencies_name": [], "description": "", "disabled": false, "explanation": null, "has_explanation": "", "icon_color": "purple", "id": "6da3ebe37baff2a6efc1404235923ebd", "index": "63", "known_lexemes": [], "language": "es", "language_string": "Spanish", "learned": false, "learning_threshold": 0, "learning_threshold_percentage": 0, "left_lessons": 0, "lesson": false, "lesson_number": 1, "locked": true, "mastered": false, "missing_lessons": 2, "more_lessons": 0, "name": "IDIOMS", "new_index": 63, "num_lessons": 2, "num_lexemes": 15, "num_missing": 15, "num_translation_nodes": 0, "path": [], "practice_recommended": false, "progress_percent": 0.0, "short": "Idioms", "strength": 0.0, "test": false, "test_count": 3, "title": "Idioms and Proverbs", "url_title": "Idioms-and-Proverbs", "words": [ "quien", "con", "m\u00e1s", "todo", "coraz\u00f3n", "se", "el", "de", "cuenta", "mal" ] } ], "calendar": [], "direction_status": "released", "first_time": true, "fluency_score": 0.0, "immersion_enabled": true, "language": "es", "language_strength": 0, "language_string": "Spanish", "level": 1, "level_left": 60, "level_percent": 0, "level_points": 60, "level_progress": 0, "level_tests": [], "max_depth_learned": 0, "max_level": false, "max_tree_level": 1, "next_lesson": { "lesson_number": 1, "skill_title": "Basics 1", "skill_url": "Basics-1" }, "next_level": 2, "no_dep": true, "num_skills_learned": 1, "placement_test": { "attempts": 1 }, "points": 0, "points_rank": 1, "points_ranking_data": [ { "avatar": "https://duolingo-images.s3.amazonaws.com/avatars/1/AX2PP_c3ls", "fullname": "", "id": 1, "language": "es", "language_string": "Spanish", "points_data": { "languages": [], "total": 0 }, "rank": 1, "self": true, "username": "admin" } ], "points_ranking_data_dict": { "1": { "avatar": "https://duolingo-images.s3.amazonaws.com/avatars/1/AX2PP_c3ls", "fullname": "", "id": 1, "language": "es", "language_string": "Spanish", "points_data": { "languages": [], "total": 0 }, "self": true, "username": "admin" } }, "skills": [ { "achievements": [], "beginner": false, "bonus": false, "category": "", "changed": false, "comment_data": {}, "coords_x": 2, "coords_y": 10, "dependencies": [ "Dates and Time" ], "dependencies_name": [ "DATES-AND-TIME" ], "description": "", "disabled": false, "explanation": null, "has_explanation": null, "icon_color": "blue", "id": "9ad994514334443ac12f950280fac0dc", "index": "45", "known_lexemes": [], "language": "es", "language_string": "Spanish", "learned": false, "learning_threshold": 0, "learning_threshold_percentage": 0, "left_lessons": 0, "lesson": false, "lesson_number": 1, "locked": true, "mastered": false, "missing_lessons": 1, "more_lessons": 0, "name": "SIZES", "new_index": 39, "num_lessons": 1, "num_lexemes": 9, "num_missing": 9, "num_translation_nodes": 0, "path": [], "practice_recommended": false, "progress_percent": 0, "short": "Sizes", "strength": 0, "test": true, "test_count": 3, "title": "Sizes", "url_title": "Sizes", "words": [ "grande", "peque\u00f1a", "largo", "altos", "cortos", "enormes", "tama\u00f1o", "gran", "bajo" ] }, ... ] "streak": 0, "tc_estimate": null, "testcenter_estimate": null } }, "languages": [ { "current_learning": false, "language": "dn", "language_string": "Dutch", "learning": false, "level": 1, "points": 0, "sentences_translated": 0, "streak": 0, "to_next_level": 60 }, { "current_learning": false, "language": "eo", "language_string": "Esperanto", "learning": false, "level": 1, "points": 0, "sentences_translated": 0, "streak": 0, "to_next_level": 60 }, { "current_learning": false, "language": "uk", "language_string": "Ukrainian", "learning": false, "level": 1, "points": 0, "sentences_translated": 0, "streak": 0, "to_next_level": 60 }, { "current_learning": false, "language": "pt", "language_string": "Portuguese", "learning": false, "level": 1, "points": 0, "sentences_translated": 0, "streak": 0, "to_next_level": 60 }, { "current_learning": false, "language": "nb", "language_string": "Norwegian (Bokm\u00e5l)", "learning": false, "level": 1, "points": 0, "sentences_translated": 0, "streak": 0, "to_next_level": 60 }, { "current_learning": false, "language": "de", "language_string": "German", "learning": false, "level": 1, "points": 0, "sentences_translated": 0, "streak": 0, "to_next_level": 60 }, { "current_learning": false, "language": "tr", "language_string": "Turkish", "learning": false, "level": 1, "points": 0, "sentences_translated": 0, "streak": 0, "to_next_level": 60 }, { "current_learning": false, "language": "fr", "language_string": "French", "learning": false, "level": 1, "points": 0, "sentences_translated": 0, "streak": 0, "to_next_level": 60 }, { "current_learning": false, "language": "da", "language_string": "Danish", "learning": false, "level": 1, "points": 0, "sentences_translated": 0, "streak": 0, "to_next_level": 60 }, { "current_learning": false, "language": "ga", "language_string": "Irish", "learning": false, "level": 1, "points": 0, "sentences_translated": 0, "streak": 0, "to_next_level": 60 }, { "current_learning": false, "language": "it", "language_string": "Italian", "learning": false, "level": 1, "points": 0, "sentences_translated": 0, "streak": 0, "to_next_level": 60 }, { "current_learning": false, "language": "sv", "language_string": "Swedish", "learning": false, "level": 1, "points": 0, "sentences_translated": 0, "streak": 0, "to_next_level": 60 }, { "current_learning": true, "language": "es", "language_string": "Spanish", "learning": true, "level": 1, "points": 0, "sentences_translated": 0, "streak": 0, "to_next_level": 60 } ], "learning_language": "es", "learning_language_string": "Spanish", "location": "", "notif_event_ids": [ "ev_2567641930", "ev_2551857838", "ev_2547818680", "ev_2541303828", "ev_2355629187" ], "num_followers": 63, "num_following": 0, "num_observees": 0, "rupees": 0, "show_dashboard_ad": true, "sina_weibo_id": null, "site_streak": 0, "streak_extended_today": false, "trial_account": false, "twitter_id": null, "ui_language": "en", "upload-self-service": false, "username": "admin" } Authenticated ````````````` When an authenticated user requests their profile, they get a lot more information. .. code-block:: javascript { "ab_options": { "android_11_google_now_experiment": true, ... }, "admin": false, "auto_facebook_post": false, "autoplay": true, "avatar": "https://s3.amazonaws.com/duolingo-images/avatar/default_2", "bio": "", "blockers": [], "blocking": [], "browser_language": "en", "calendar": [ { "datetime": 1435894795000.0, "improvement": 10 }, ... ], "certificates": [ { "datetime": "\n\n\n\n\n\n\n\n1 month ago\n\n", "id": "abcde", "language": "de", "language_string": "German", "score": 2.09 } ], "change-design": false, "cohort": null, "created": "\n\n\n\n\n\n\n\n\n3 years ago\n", "created_dt": 1338352674000.0, "creation_date": "2012-05-30T04:37:54", "current_time": 1436510650294.148, "daily_goal": 30, "dashboard_redesign": true, "deactivated": false, "delete-permissions": false, "email": "me@gmail.com", "events": [], "facebook_id": "123456789", "filter_stream": false, "followers": [], "following": [], "freeze-permissions": false, "fullname": null, "gplus_id": null, "has_google_now": false, "has_observer": false, "id": 1234, "insite_immersion_lingots": true, "insite_sentence_edited": true, "inventory": { "flirting_de": "2015-04-18 02:48:18", "formal_outfit": "2015-05-21 11:19:08", "idioms_de": "2015-02-16 02:18:53", "streak_freeze": "2015-05-14 05:53:16", "timed_practice": "grandfathered" }, "invites": { "extra_invites": 0, "invited": [], "invites_left": 3 }, "is_following": false, "is_self_observer": false, "language_data": { "de": { "all_time_rank": [ "1234", "9999999", "1111111" ], "bonus_rows": [ 4 ], "bonus_skills": [ { "achievements": [], "beginner": false, "bonus": true, "category": "bonus", "changed": false, "comment_data": {}, "coords_x": 1, "coords_y": 1000, "dependencies": [], "dependencies_name": [], "description": "", "disabled": false, "explanation": null, "has_explanation": "", "icon_color": "purple", "id": "47cc154e7d091c46aefb18dbf583588d", "index": "63", "known_lexemes": [ "0e31a5f2b119b3f4b7b6eac64cc8e98c", "4ae59c85701323605ad9c5e8d35a81cf", "4d19a63831881edce7a42efbbe9b0cbc", "54ca03339b893eb13bd2c0e699270ea3", "625507b03d9d3d674f0aafe9a9b83c6d", "793979dd80dd32ccba22a4ccf09d6dc6", "8217bc2911be463e40f8277fbaad38e1", "a69bac60383f632f46c89abb28c1be6b", "b1758fb28f93586d6f2373c24cc2850e", "c6bae3eda56c6c57292868a950657a0a", "d26d14bf5202ced0f7ccf510c1e9379a", "e5d5b55f107acc1e69eaf98cba857a1a", "eb975e9337b1c55de88aa89d508746d2", "fdaac4e11cfd5ba2e2d4ac5088434a5a", "fdd9e771d1ed33fde73aeeabf8e587ab" ], "language": "de", "language_string": "German", "learned": true, "learned_ts": 1424424687, "learning_threshold": 0, "learning_threshold_percentage": 0, "left_lessons": 0, "lesson": false, "lesson_number": 1, "locked": false, "mastered": true, "missing_lessons": 0, "more_lessons": 0, "name": "IDIOMS", "new_index": 63, "num_lessons": 2, "num_lexemes": 15, "num_missing": 0, "num_translation_nodes": 0, "path": [], "practice_recommended": false, "progress_percent": 100.0, "short": "Idioms", "strength": 1, "test": false, "test_count": 3, "title": "Idioms and Proverbs", "url_title": "Idioms-and-Proverbs", "words": [ "Meister", "wir", "du", "nicht", "in", "alles", "sagt", "das", "es", "ich" ] }, { "achievements": [], "beginner": false, "bonus": true, "category": "bonus", "changed": false, "comment_data": {}, "coords_x": 2, "coords_y": 1000, "dependencies": [], "dependencies_name": [], "description": "", "disabled": false, "explanation": null, "has_explanation": "", "icon_color": "purple", "id": "12bf924c1fae71c723e86dd20b377928", "index": "64", "known_lexemes": [ "0021362e7e6c2ca9660d9c6ce58a3fea", "0795b250299434fc8335f3f18f4a22ab", "07b77395a5523aaa6424b797a8052bc8", "08d5aedbf7533146786bca797c68789d", "3879a2c7c57d026ce0d63028f4a7ac97", "48fceec852338522f29d2c401224005e", "5523a9bfb978db8f5cd0f5fda429c494", "61d42d5a55018d2e1e1585cc57324702", "624ed6195d86d1450165295ab875e171", "671aff64046a3598f82f6cc7a1d2fbc2", "9cb6715e32862480033e330c5b4971cb", "a497fedbde428c113aadbe6ab4b2cc27", "acd4b1188d2ecae1f735ef56308ade2f", "bad3ea3a35e041def877b10ce1e249b1", "ea3155c1930625d2ac0a7f1f7a941cdd" ], "language": "de", "language_string": "German", "learned": true, "learned_ts": 1429326405, "learning_threshold": 0, "learning_threshold_percentage": 0, "left_lessons": 0, "lesson": false, "lesson_number": 1, "locked": false, "mastered": true, "missing_lessons": 0, "more_lessons": 0, "name": "FLIRTING", "new_index": 64, "num_lessons": 2, "num_lexemes": 15, "num_missing": 0, "num_translation_nodes": 0, "path": [], "practice_recommended": false, "progress_percent": 100.0, "short": "Flirting", "strength": 1, "test": false, "test_count": 3, "title": "Flirting", "url_title": "Flirting", "words": [ "deine", "darf", "m\u00f6chte", "dich", "wir", "nicht", "n\u00e4chste", "s\u00fc\u00df", "bist", "will" ] } ], "calendar": [ { "datetime": 1435894795000.0, "improvement": 10 }, { "datetime": 1435894995000.0, "improvement": 10 }, ... ], "direction_status": "released", "first_time": false, "fluency_score": 0.4546002196, "immersion_enabled": true, "language": "de", "language_strength": 0.96, "language_string": "German", "level": 14, "level_left": 66, "level_percent": 95, "level_points": 1500, "level_progress": 1434, "level_tests": [ { "attempts": 3, "index": 0, "level": 6 }, { "attempts": 3, "index": 1, "level": 11 }, { "attempts": 3, "index": 2, "level": 22 }, { "attempts": 3, "index": 3, "level": 28 } ], "max_depth_learned": 29, "max_level": false, "max_tree_level": 30, "next_lesson": { "lesson_number": 1, "skill_title": "Abstract Objects 1", "skill_url": "Abstract-Objects-1" }, "next_level": 15, "no_dep": true, "notifications": { "chrome_app_ad": false, "duolingo_for_schools": false, "lesson_strength_bar": false }, "notify_practice": false, "notify_time": 1020, "num_global_practice": 1, "num_skills_learned": 57, "placement_test": { "attempts": 1 }, "points": 7434, "points_rank": 1, "points_ranking_data": [ { "avatar": "https://s3.amazonaws.com/duolingo-images/avatar/default_2", "fullname": null, "id": 1234, "language": "de", "language_string": "German", "points_data": { "languages": [], "total": 9651 }, "rank": 1, "self": true, "username": "me" }, { "avatar": "https://s3.amazonaws.com/duolingo-images/avatar/default_2", "fullname": "My Friend", "id": 9999999, "language": "de", "language_string": "German", "points_data": { "languages": [], "total": 3000 }, "rank": 2, "self": false, "username": "friend" }, ... }, "push_practice": true, "show_practice": true, "skills": [ ... ], "streak": 58, "tc_estimate": null, "testcenter_estimate": null, "tracking_properties": { "direction": "de<-en", "learning_language": "de", "max_tree_level": 30, "took_placementtest": false, "ui_language": "en" } } }, "languages": [ ... ], "learning_language": "de", "learning_language_string": "German", "location": null, "microphone": false, "notif_event_ids": [], "notify_activity_comment": true, "notify_activity_reply": true, "notify_announcement": true, "notify_comment": true, "notify_edit_suggested": true, "notify_follow": true, "notify_pass": true, "notify_stream_post": true, "num_followers": 5, "num_following": 8, "num_observees": 0, "push_activity_comment": true, "push_activity_reply": true, "push_announcement": true, "push_comment": true, "push_edit_suggested": true, "push_follow": false, "push_passed": false, "push_stream_post": true, "rupees": 349, "show_dashboard_ad": true, "sina_weibo_id": null, "site_streak": 142, "sound_effects": true, "speaker": true, "streak_extended_today": true, "timezone": "America/Los_Angeles", "timezone_offset": "-0700", "tracking_properties": { "creation_date_new": "2012-05-30T04:37:54", "has_google_now": false, "has_observer": false, "is_self_observer": false, "lingots": 349, "num_observees": 0, "trial_account": false, "user_id": 1234, "username": "me" }, "trial_account": false, "twitter_id": null, "ui_language": "en", "upload-self-service": false, "username": "me", "week_old_account": true } ``GET /store/get_items`` ------------------------ **Requires authentication.** This endpoint just returns a list of all items available in the store. It's not entirely clear to yet exactly how the app tracks which items have been bought, equipped, are available on that platform, etc. Interestingly, their human-backed certification is included in this API response. .. code-block:: javascript { "bonus_skills": [ { "description": "Learn some German idioms and proverbs.", "icon_index": 63, "name": "idioms", "price": 30, "skill_id": "47cc154e7d091c46aefb18dbf583588d", "title": "Idioms and Proverbs" }, { "description": "Do you believe in love at first sight? Learn to flirt in German.", "icon_index": 64, "name": "flirting", "price": 30, "skill_id": "12bf924c1fae71c723e86dd20b377928", "title": "Flirting" } ], "misc_items": [ { "name": "certification_human", "price": 100 }, { "description": "Streak Freeze allows your streak to remain in place for one full day of inactivity.", "name": "streak_freeze", "price": 10, "title": "Streak Freeze" }, { "name": "certification", "price": 25 }, { "description": "Attempt to double your five lingot wager by maintaining a seven day streak.", "name": "rupee_wager", "price": 5, "title": "Double or Nothing" }, { "name": "timed_practice", "price": 10 }, { "description": "Allows you to regain one heart lost during a lesson.", "name": "heart_refill", "price": 4, "title": "Heart Refill" } ], "outfits": [ { "description": "Learn in style. Duo has always been a sharp guy, now he'll look sharp too.", "name": "formal_outfit", "price": 20, "title": "Formal Attire" }, { "description": "Learn in luxury. Duo will love the feel of 24 carat gold silk on his feathers.", "name": "luxury_outfit", "price": 30, "title": "Champagne Tracksuit" } ] } ``POST /me/switch_language`` ---------------------------- This endpoint is used to switch languages (imagine that!). To switch, it requires a form-encoded parameter ``learning_language``, which should be the language you want to learn, like ``de``. It can also take a parameter ``from_language``, which should be the two letter language you want to learn from, like ``en``. :: POST /me/switch_language learning_language: fr Its response body is equivalent to the ``language_data/language`` part of the ``/users/show`` endpoint shown above. ``GET /dictionary/hints/{target}/{source}?tokens=["word1","word2",...] ---------------------------------------------------------------------- Oddly, this endpoint operates on a different base URL. The base URL can be gotten through ``/version_info``. It takes the query string as shown above, and returns a result like this: .. code-block:: javascript { bonjour: [ "hello", "good morning", "good afternoon", "good day" ], monde: [ "world", "people", "anyone", "crowd", "set", "society" ] } ``skills`` ---------- ``activity stream`` -------------------