Duolingo Scraper - Profiles, Leaderboards, Courses & Vocabulary
Pricing
from $1.80 / 1,000 learner records
Duolingo Scraper - Profiles, Leaderboards, Courses & Vocabulary
Scrape public Duolingo data without login. Extract learner profiles with streaks, XP and achievements, weekly league standings, course catalogs with learner counts, and vocabulary words from lessons in clean structured data.
Pricing
from $1.80 / 1,000 learner records
Rating
0.0
(0)
Developer
Abot API
Maintained by CommunityActor stats
1
Bookmarked
3
Total users
0
Monthly active users
22 days ago
Last modified
Categories
Features
Share
Duolingo Scraper: Profiles, Leagues, Courses & Vocabulary
Duolingo Scraper turns Duolingo into structured learning data. Get learner profiles by username (streak, total XP, per-course XP and crowns, achievements), weekly league standings, the full public course catalog with learner counts, or word and translation rows from a course's lessons. Search by username, paste profile links, browse the catalog, or walk a course's vocabulary, then export to JSON, CSV or Excel, or pull the results straight into your app through the API. Every surface this actor reads works with no Duolingo account at all.
Why This Scraper?
- Four ways to read the platform. Search by username (profiles or weekly league), paste profile links, browse the full course catalog, or read a course's vocabulary lesson by lesson.
- No account needed anywhere. Profiles, leagues and the course catalog read anonymously; achievements detail and the vocabulary walk use a viewer session the actor mints for itself. You never paste a login or a token.
- Deep learner profiles. Streak, total XP, per-course XP and crowns, join date, Super subscription flag, location, motivation, roles and full achievement badge detail.
- The whole course catalog. 300+ language pairs with learner counts, release maturity and progress, filterable by keyword, language pair or learner floor, sorted by popularity or name.
- Vocabulary reading. Walk a course's active lessons and collect word and translation pairs with their skill, unit and level context.
- Built for schedules. Incremental mode returns only new and changed learners on recurring runs, and a refused run fails loudly instead of returning an empty dataset.
- Resume large pulls. Continue an interrupted run from its run or dataset ID without paying twice for records you already have.
Use Cases
- Language-learning coaches and classrooms: track streaks, XP and course progress for a roster of learners over time.
- Community and leaderboard tools: build league standing dashboards or streak leaderboards from public profiles.
- Course research and content teams: compare learner counts, release maturity and language pairs across the full catalog.
- Vocabulary and curriculum builders: extract word and translation lists from specific lessons for flashcards or study guides.
- Product and market research: watch which language pairs are growing by learner count over time with incremental mode.
Data You Get
Sample shape: values are illustrative placeholders, not from a live profile.
| Field | Example |
|---|---|
kind | "profile" (also "league_summary", "course", "vocabulary") |
recordId | "200000042" |
username | "sample_learner" |
name | "Jordan Learner" |
userId | 200000042 |
profileUrl | "https://www.duolingo.com/profile/sample_learner" |
joinedAt | 1700000000 |
streak | 42 |
totalXp | 8500 |
learningLanguage / fromLanguage | "es" / "en" |
hasPlus | false |
location | "Sample City" |
bio | "Learning one language at a time." |
courses | [{"title": "Spanish", "xp": 8000, "crowns": 45}] |
coursesCount | 2 |
achievements | [{"name": "sample_badge", "tier": 2, "count": 15}] |
achievementsDetail | [{"name": "sample_badge", "tier": 2, "count": 15, "rewards": {}, "tierCounts": [5, 15, 50]}] (null when switched off) |
achievementsCount | 2 |
motivation | "CAREER" |
roles | [] |
changeType | "NEW" |
League runs emit kind: "league_summary" rows with tier, highestLeague, numWins, numberOneFinishes, numberTwoFinishes, topThreeFinishes, streakInTier and lastWinContestEnd. Course-catalog runs emit kind: "course" rows with learningLanguageName, fromLanguageName, numLearners, releaseStatus, courseUrl, plus url (the same course link), scrapedAt and error (always null on a returned row; a read that fails fails the run instead of returning an error row). Vocabulary runs emit kind: "vocabulary" rows with word, translation, skillName, unitIndex and sectionIndex.
How to Use
- Pick a mode:
search(profiles or weekly league by username),url(paste profile links),courses(the full catalog) orvocabulary(word rows from one course). - Fill in the fields for that mode, and switch on the profile filters or achievements detail you need.
- Set Max items to control run size and cost, then click Start.
- Download the dataset as JSON, CSV or Excel, or read it through the API.
Read a set of public profiles:
{"mode": "search","searchType": "profiles","usernames": ["sample_learner", "another_sample"]}
Read weekly league standings for a set of learners:
{"mode": "search","searchType": "leaderboard","usernames": ["sample_learner"]}
List the course catalog (Spanish courses only here, most learners first):
{"mode": "courses","courseQuery": "spanish","courseSort": "learners","maxItems": 50}
Read vocabulary from a course pair:
{"mode": "vocabulary","courseLearningLanguage": "es","courseFromLanguage": "en","maxItems": 100}
Run it from your code
Python:
from apify_client import ApifyClientclient = ApifyClient("<YOUR_APIFY_TOKEN>")run = client.actor("abotapi/duolingo-learner-scraper").call(run_input={"mode": "search", "searchType": "profiles", "usernames": ["sample_learner"]})for profile in client.dataset(run["defaultDatasetId"]).iterate_items():print(profile["username"], profile["streak"], profile["totalXp"])
JavaScript:
import { ApifyClient } from 'apify-client';const client = new ApifyClient({ token: '<YOUR_APIFY_TOKEN>' });const run = await client.actor('abotapi/duolingo-learner-scraper').call({ mode: 'search', searchType: 'profiles', usernames: ['sample_learner'] });const { items } = await client.dataset(run.defaultDatasetId).listItems();
Or connect it to Make, Zapier, n8n, Google Sheets or webhooks from the Integrations tab.
Achievements, vocabulary and the self-minted session
Achievements detail (on by default) and the vocabulary walk both need a viewer session. The actor mints its own automatically: a short, one-time onboarding step with no captcha, no email and no signup form, then every read replays over plain HTTP. The session is reused across runs and refreshed when stale; a failed mint fails the run loudly instead of shipping empty rows. You never paste a credential, and Profiles search, League search and Course-catalog mode never need this step at all.
Resume and recurring updates
- Resume (
resumeFromRunId) continues one interrupted run: paste its run or dataset ID and the actor skips everything already collected there, so you don't pay twice. - Incremental mode (
incrementalMode) is for scheduled runs over the same search. Each record is classifiedNEW,UPDATED(withchangedFields),UNCHANGED(suppressed and not billed unlessemitUnchangedis on),REAPPEAREDorEXPIRED(only after a run that scanned the whole tracked scope, and only withemitExpired).stateKeynames or shares the stored state; if you share one state across runs that each cover only part of the tracked learners, leaveemitExpiredoff, or the learners a run did not cover come back asEXPIRED. Streak and XP are part of change detection, so a learner who practised since the last run comes backUPDATED; that is the progress signal. Live activity flags, the streak detail object, per-course XP, the catalog learner counter and the scrape timestamp are returned but never counted as a change;ignoreFieldsForChangesexcludes more, and a changed ignore list causes no one-off wave ofUPDATEDrows. With incremental mode off, output is exactly as before.
Send results into your apps (MCP connectors)
Optionally pipe the scraped results into the apps you already use, via Model Context Protocol (MCP) connectors. This is an extra delivery step after the scrape: the Apify dataset is never changed.
What gets written to the connector: a condensed, human-readable summary of each record, not the full JSON. Each item becomes one entry with a title and its key fields flattened to plain text. The complete record always stays in the Apify dataset.
- Authorize a connector once under Apify, Settings, Integrations (Notion, Linear, Airtable, or Apify).
- Select it in the "Pipe results into your apps" input field. (If the picker is empty, you haven't authorized a connector yet.)
- For Notion, also set
notionParentPageUrlto the page where items should be created.
The connection is mediated by Apify's MCP proxy, so this actor never sees your third-party credentials. Leave the field empty to skip.
Input Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
mode | string | search | search, url, courses, or vocabulary. |
searchType | string | profiles | profiles (by username) or leaderboard (weekly league, by username). Search mode only. |
usernames | array | ["duolingo"] | Usernames to read (Profiles and League search types). A username nobody matches is reported, not failed. |
urls | array | six example links, one per link shape | Links to read in URL mode. When you leave the field untouched, the six example links below are used, and each resolves to a different learner record. See the URL shapes table. |
courseQuery | string | empty | Only keep courses whose language names or codes contain this keyword (Course-catalog mode). |
courseLearningLanguage | string | empty | Course pair selector: the learning language code (Course-catalog and Vocabulary modes). |
courseFromLanguage | string | empty | Course pair selector: the source language code the course is taught from (Course-catalog and Vocabulary modes). |
courseMinLearners | integer | 0 | Only keep courses with at least this many learners (0 = no floor; Course-catalog mode). |
courseSort | string | learners | Course row order: most learners first, or language name A to Z (Course-catalog mode). |
vocabSkill | string | empty | Only read lessons whose skill label contains this keyword (Vocabulary mode). |
learningLanguage | string | empty | Only return profiles learning this language code (empty = all; applied to results). |
hasPlusOnly | boolean | false | Only return profiles with a paid Super subscription. |
minTotalXp | integer | 0 | Only return profiles with at least this much total XP (0 = no floor). |
minStreak | integer | 0 | Only return profiles with an active streak of at least this many days (0 = no floor). |
fetchAchievements | boolean | true | Add each profile's full achievements detail: one extra read per profile. |
maxItems | integer | 20 | Stop after this many records in total. 0 means the hard cap of 100000 records. |
resumeFromRunId | string | (none) | Paste a previous run ID or dataset ID to continue a large pull without duplicate records. |
incrementalMode | boolean | false | Turn this on for daily or recurring monitoring: later runs return only what changed. |
stateKey | string | (none) | Optional. Name a monitoring campaign, or share state across differently configured runs (incremental mode only). |
emitUnchanged | boolean | false | Also return (and bill) unchanged records (incremental mode only). |
emitExpired | boolean | false | Also return (and bill) records no longer found, after a complete scan (incremental mode only). |
ignoreFieldsForChanges | array | [] | Field names (or dotted paths such as courses.crowns) that never count as a change in incremental mode. They are still returned in the output (incremental mode only). |
mcpConnectors | array | (none) | Optional: send a summary of each record to apps you authorized under Integrations. |
notionParentPageUrl | string | (none) | Notion connector only: page under which item pages are created. |
maxNotifyListings | integer | 50 | Cap on items written to each connector per run. Does not affect the dataset. |
proxyConfiguration | object | Apify Proxy | Apify Proxy settings; the default works on every plan. |
URL shapes accepted in URL mode
| Link shape | Example | Result |
|---|---|---|
| Profile page | https://www.duolingo.com/profile/duolingo | One learner record. |
| Profile page without www | https://duolingo.com/profile/luisvonahn | One learner record. |
| Profile page with a sub-path | https://www.duolingo.com/profile/Duo/courses | One learner record. |
| Users API by username | https://www.duolingo.com/2017-06-30/users?username=Bernie | One learner record. |
| Short id link | https://www.duolingo.com/u/1723277568 | One learner record when the account is readable. |
| Numeric users API path | https://www.duolingo.com/2017-06-30/users/1723277568 | Skipped with a warning in the run log (needs an account). |
| Course catalog page | https://www.duolingo.com/courses | Skipped with a warning: use Course catalog mode. |
If none of the pasted links can be read, the run fails with a message listing the supported shapes; if only some can be read, the others are skipped with a warning and the run status message counts them. The same learner pasted in several shapes is read once. If you leave URLs untouched, the six example profile links are used (a change from earlier versions, where the field was empty): they resolve to six different learners, one record each. Clear the field and list your own links to read something else.
Output views. The dataset has five views in the Console: Results (all rows), Profiles, League, Course catalog and Vocabulary. The Output tab links to each one.
Output Example
Sample shape: values are illustrative placeholders, not from a live profile.
{"kind": "profile","recordId": "200000042","username": "sample_learner","name": "Jordan Learner","userId": 200000042,"profileUrl": "https://www.duolingo.com/profile/sample_learner","bio": "Learning one language at a time.","joinedAt": 1700000000,"streak": 42,"totalXp": 8500,"learningLanguage": "es","fromLanguage": "en","currentCourseId": "SAMPLE_ES_EN","hasPlus": false,"location": "Sample City","coursesCount": 2,"courses": [{"id": "SAMPLE_ES_EN", "title": "Spanish", "learningLanguage": "es", "fromLanguage": "en", "xp": 8000, "crowns": 45},{"id": "SAMPLE_FR_EN", "title": "French", "learningLanguage": "fr", "fromLanguage": "en", "xp": 500, "crowns": 3}],"achievements": [{"name": "sample_badge", "tier": 2, "count": 15},{"name": "sample_streak_badge", "tier": 1, "count": 7}],"achievementsCount": 2,"achievementsDetail": [{"name": "sample_badge", "tier": 2, "count": 15, "rewards": {}, "tierCounts": [5, 15, 50]},{"name": "sample_streak_badge", "tier": 1, "count": 7, "rewards": {}, "tierCounts": [7, 30, 100]}],"motivation": "CAREER","roles": []}
Plan Requirement
Reading needs Apify Proxy, and the actor measured as working through the datacenter proxy group, so residential is not required. The default proxy setting is enough for every mode. Profile reads that need achievements detail or the vocabulary walk briefly mint a viewer session using the same connection settings; no extra plan tier is needed for that.
Apify Proxy is not available on the free plan. If the proxy connection is refused for your account, the run stops with this message: "The proxy connection was refused for this account, so no page could be read. Your Apify plan does not include proxy access; a paid plan is required: https://apify.com/pricing. If you entered your own proxy URLs, check that their credentials are valid." Nothing is silently returned as an empty dataset.
FAQ
How much does it cost?
You pay a small start fee plus a fee per record returned. Achievements detail adds no separate charge; it only adds one extra read per profile. The Pricing tab shows the current rates. Use Max items to cap the cost of any run.
Is it legal to scrape Duolingo?
This actor collects only publicly available learner and course data. You are responsible for how you use it: follow Duolingo's terms and the laws that apply to you, and get legal advice if you plan commercial redistribution.
Can I get only new or changed learners on a schedule?
Yes. Schedule the actor from the Schedules tab and turn on Incremental mode. Each run then returns only new, updated and reappeared records, and unchanged ones are not billed.
Why can't I search for a learner without knowing their username?
The source has no anonymous keyword search for people: profiles are read by exact username, or by pasting a profile link. Course-catalog mode does support a keyword search, since courses (not people) are the thing being searched there.
Why did my run fail instead of returning an empty dataset?
If the site refuses every request, or no results page could be loaded ("Could not load any results page (the requests failed), so nothing was collected. This is a connection problem, not an empty search."), the run stops with a clear message so "no matches" is never confused with "nothing could be read". Run it again in a few minutes. If only some usernames or links could be read, you still get what was found.
Can I use it with AI agents or MCP?
Yes. Call it from any Apify integration or MCP client, and use the connector field to push results into Notion, Linear or Airtable.
๐ More scrapers you might like
Pair this actor with these related scrapers from the same team:
| ๐ผ Superprof Scraper Scrape Superprof tutors by subject and location, or paste tutor URLs. One rich record per... | ๐ผ Reed.co.uk Scraper Scrape Reed.co.uk jobs, companies, courses and reviews by keyword, location, filters or... |
| โฝ OneFootball Scraper Scrape public OneFootball data including football news, upcoming fixtures, match results... | โฝ FotMob Scraper โฝ Match Details, Stats, Lineups & Players Pull structured football data from FotMob: match details with scores, team stats, events... |
| ๐งฉ WEBTOON Scraper Scrape the WEBTOON catalog into structured data. Extract series, genres, rankings and... | โฝ Globo Esporte Scraper Scrape public sports content from ge.globo.com, including news, videos, matches and feed... |
๐ Browse all abotapi scrapers
๐ฌ Support
- ๐ Found a bug or a missing field? Open a ticket on the Issues tab. We usually reply within hours.
- ๐ ๏ธ Need another site, extra fields or a private build? Email contact@abotapi.com.
- โญ Enjoying it? A quick review on the actor page helps other users find it.