The RAE Dictionary API: An Open Secret Hidden in Plain Sight
How I reverse-engineered the Spanish Royal Academy's Android app and found an API used by dozens of projects, with hardcoded credentials that have been public for years.
The RAE Dictionary API - An Open Secret Hidden in Plain Sight
How I reverse-engineered the Spanish Royal Academy’s Android app and found an API used by dozens of projects
Introduction: A Missing Public API
The Royal Spanish Academy (Real Academia Española or RAE) is the official institution responsible for regulating the Spanish language. Their dictionary, the Diccionario de la Lengua Española (DLE), is the definitive reference for over 500 million Spanish speakers worldwide.
For developers, there’s a problem: RAE does not offer an official public API, at least that I know of.
If you want to integrate Spanish dictionary lookups into your application, you’re left with a few unattractive options:
- Web scraping (or unofficial scrapers) — unreliable, brittle, and ethically questionable
- Using third-party dictionaries — which may be outdated, unofficial, or incomplete
- Reverse-engineering the official app — which is exactly what I did.
This writeup documents that journey, including a surprising discovery: the exact same authentication credentials I found are used by at least a dozen public GitHub repositories, meaning this “secret” API has been an open secret for nearly a decade.
Part 1: Decompiling the Android App
The Target
The official RAE Android app (es.rae.dle) is available on the Google Play Store. Like most Android apps, it’s written in Java/Kotlin and compiled into an APK file.
The Tools
- JADX-GUI: A decompiler that turns APK files back into readable Java code
- Search skills: Looking for relevant strings and patterns
The First Clue
After loading the APK into JADX-GUI, I searched for terms related to HTTP authentication:
"Authorization""Basic""Base64""dle.rae.es"
In Utils.java, I found something interesting:
public static String getCabeceraBAA() {
return Base64.encodeToString("p682JghS3:aGfUdCiE434".getBytes(), 2);
}
This function returns a Base64-encoded version of "p682JghS3:aGfUdCiE434". That’s clearly a username:password pair.
The Code That Uses It
public static String conexionAServiciosHttps(String str) {
// ... setup ...
httpsURLConnection.setRequestProperty("Authorization", "Basic " + getCabeceraBAA());
// ... execute request ...
}
That’s it. The app authenticates to the backend using HTTP Basic Authentication with hardcoded credentials. No token rotation. No per-user authentication. Nothing.
The Decoded Credentials
Let’s decode the Base64 string:
cDY4MkpnaFMzOmFHZlVkQ2lFNDM0
Decoded:
p682JghS3:aGfUdCiE434
Username: p682JghS3
Password: aGfUdCiE434
These credentials are embedded in the app binary. Anyone with a decompiler can extract them.
Part 2: Understanding the API
Base URL
The app defines its server endpoint as:
<string name="servidor">https://dle.rae.es/</string>
Required Headers
To successfully query the API, you must send these headers:
Authorization: Basic cDY4MkpnaFMzOmFHZlVkQ2lFNDM0
User-Agent: Dalvik/2.1.0 (Linux; U; Android 9; Xperia Z1 Compact Build/PQ3A.190605.003)
Important: The User-Agent is critical. Without it, the server returns a 401 Unauthorized error even with correct credentials. The server validates both the authentication and the client type.
The Endpoints
| Endpoint | Method | Purpose | Parameters |
|---|---|---|---|
/data/search |
GET | Search for words | w (word), m (mode) |
/data/fetch |
GET | Get full definition | id (word ID) |
/data/random |
GET | Random word | - |
/data/wotd |
GET | Word of the day | callback=json |
/data/keys |
GET | Autocomplete suggestions | q (query), fc=1 |
/data/anagram |
GET | Find anagrams | w (word) |
/data/ids |
GET | Get IDs by word | w (word) |
/data/header |
GET | Get word header | id |
Search Modes (the m parameter)
| Mode | Value | Example |
|---|---|---|
| Exact | 30 |
/data/search?w=casa&m=30 |
| Starts with | 31 |
/data/search?w=cas&m=31&f=1&t=200 |
| Ends with | 32 |
/data/search?w=ando&m=32&f=1&t=200 |
| Contains | 33 |
/data/search?w=rr&m=33&f=1&t=200 |
| Expressions | 16 |
/data/search?w=agua&m=16&f=1&t=200 |
Example: Searching for “casa”
curl -X GET "https://dle.rae.es/data/search?w=casa&m=30" \
-H "Authorization: Basic cDY4MkpnaFMzOmFHZlVkQ2lFNDM0" \
-H "User-Agent: Dalvik/2.1.0 (Linux; U; Android 9; Xperia Z1 Compact Build/PQ3A.190605.003)"
Response:
{
"approx": 0,
"res": [
{
"header": "casa",
"id": "D1g6sC6",
"grp": 0
}
]
}
Example: Fetching a Definition
curl -X GET "https://dle.rae.es/data/fetch?id=D1g6sC6" \
-H "Authorization: Basic cDY4MkpnaFMzOmFHZlVkQ2lFNDM0" \
-H "User-Agent: Dalvik/2.1.0 (Linux; U; Android 9; Xperia Z1 Compact Build/PQ3A.190605.003)"
Response: Full HTML document with the structured definition.
Part 3: The HTML Structure
The API doesn’t return JSON for definitions. Instead, /data/fetch returns HTML with specific CSS classes that the Android app renders in a WebView.
<article id="D1g6sC6">
<header class="f">casa</header>
<p class="n2">Del lat. <em>casa</em>.</p>
<p class="j" id="...">
<span class="n_acep">1. </span>
<abbr class="d" title="sustantivo femenino">f.</abbr>
Edificio para habitar.
<a class="a" href="/?id=...">ver también</a>
</p>
</article>
Key CSS classes:
header.f— the headword (lema)p.n2— etymologyp.j— definition (acepción)span.n_acep— definition numberabbr.d— grammatical category (abbreviation)a.a— cross-reference to another word
This HTML is designed to be displayed in a WebView, with CSS from https://dle.rae.es/css/drae.css providing the styling.
Part 4: The Discovery — This API Is Everywhere
After figuring out the API, I decided to search GitHub for the Base64 authentication token I had found:
Basic cDY4MkpnaFMzOmFHZlVkQ2lFNDM0
I found at least 11 public repositories containing these exact credentials.
| Repository | Language | Year (approx) |
|---|---|---|
mgp25/RAE-API |
PHP | 2018 |
account0123/RAE-API |
JavaScript | 2019 |
romancitodev/fewwis-bot |
Rust | 2020 |
Dellos7/DDRAE_TelegramBot |
TypeScript | 2021 |
josago97/RAE.NET |
C# | 2019 |
escuelaces/CifrasLetras |
C# | 2020 |
fanchymarin/wotd-rae |
XML (Android) | 2018 |
danflopss/gramatica |
JavaScript | 2020 |
victor141516/RAE-API |
JavaScript | 2020 |
Chuusi/give-a-word |
JavaScript | 2021 |
RUGSoftEng/QualityExamplesExtractor |
Python | 2017 |
This means the “secret” API has been exposed and exploited by developers since at least 2017.
They likely found the credentials in the web version of the app or through decompiling the APK, just like I did. The credentials have never been rotated or revoked.
This endpoint however doesnt seem to be used in the official RAE website anymore, but just in the legacy app
What’s even more interesting?
Some repositories use a direct IP address instead of the domain:
const BASE_URL = 'https://85.62.86.187/data/';
This is from an older version of the app where the IP was hardcoded before they switched to using the domain name. This confirms that these repositories originated from decompiling the official app.
Part 5: The Irony — This API Is No Longer Used on the Official Website
Here’s the twist: if you go to the RAE’s official website (https://dle.rae.es) today, you won’t find these API endpoints being called.
The modern web version uses a completely different backend architecture. The /data/search and /data/fetch endpoints are only active for the legacy Android app.
This means:
- The endpoints still work — presumably to support older app versions
- They’re maintained but not updated — the API surface hasn’t changed
- The credentials remain the same — nobody at RAE has ever rotated them
This is a classic case of “it works, don’t touch it” — but from a security perspective, it’s a nightmare.
Part 6: A Simple Python Client
Here’s a complete, working Python client that demonstrates how to use the API:
import requests
import base64
from pathlib import Path
from bs4 import BeautifulSoup
# Credentials extracted from the app
AUTH = base64.b64encode(b'p682JghS3:aGfUdCiE434').decode()
HEADERS = {
'Authorization': f'Basic {AUTH}',
'User-Agent': 'Dalvik/2.1.0 (Linux; U; Android 9; Xperia Z1 Compact Build/PQ3A.190605.003)'
}
BASE = 'https://dle.rae.es'
CACHE_DIR = Path.home() / '.cache' / 'rae'
def search_word(word, mode=30):
"""
Search for a word in the RAE dictionary.
mode=30: exact match
mode=31: starts with
mode=32: ends with
mode=33: contains
"""
url = f'{BASE}/data/search?w={word}&m={mode}'
resp = requests.get(url, headers=HEADERS)
return resp.json() if resp.status_code == 200 else None
def get_definition(word_id, use_cache=True):
"""Fetch the full HTML definition for a word ID."""
cache_file = CACHE_DIR / f'{word_id}.html'
if use_cache and cache_file.exists():
return cache_file.read_text(encoding='utf-8')
url = f'{BASE}/data/fetch?id={word_id}'
resp = requests.get(url, headers=HEADERS)
if resp.status_code == 200:
cache_file.parent.mkdir(parents=True, exist_ok=True)
cache_file.write_text(resp.text, encoding='utf-8')
return resp.text
return None
def parse_definition(html):
"""Extract structured data from the definition HTML."""
soup = BeautifulSoup(html, 'html.parser')
article = soup.find('article')
if not article:
return None
header = article.find('header', class_='f')
lema = header.text.strip() if header else ''
etim = article.find('p', class_='n2')
etimologia = etim.text.strip() if etim else ''
definitions = []
for p in article.find_all('p', class_='j'):
num = p.find('span', class_='n_acep')
numero = num.text.strip() if num else ''
if num:
num.decompose()
categories = [abbr.text.strip() for abbr in p.find_all('abbr', class_='d')]
for abbr in p.find_all('abbr', class_='d'):
abbr.decompose()
links = [a.text.strip() for a in p.find_all('a', class_='a')]
for a in p.find_all('a', class_='a'):
a.replace_with(a.text)
text = ' '.join(p.text.split())
definitions.append({
'number': numero,
'categories': categories,
'definition': text,
'references': links
})
return {
'headword': lema,
'etymology': etimologia,
'definitions': definitions
}
# Example usage
result = search_word('casa')
if result and result.get('res'):
word_id = result['res'][0]['id']
html = get_definition(word_id)
if html:
parsed = parse_definition(html)
print(f"đź“– {parsed['headword']}")
print(f" Etymology: {parsed['etymology']}")
for d in parsed['definitions'][:3]:
print(f" {d['number']} {d['definition']}")
Part 7: The Security Implications
This discovery highlights a fundamental security flaw:
What RAE Did Wrong
- Hardcoded credentials — Authentication secrets should never be embedded in client-side code.
- No credential rotation — These credentials appear to have been unchanged since at least 2017.
- Single set of credentials — All users of the app share the same authentication token.
- No rate limiting or monitoring — There appears to be no protection against mass scraping.
What This Means for the RAE
- Anyone can scrape the dictionary — No permission or official API key required.
- The server is vulnerable to abuse — Without rate limiting, it could be taken down by a determined scraper.
- No accountability — There’s no way to track who is making requests.
How to Fix It
- Implement OAuth2 with user-specific tokens (or at least API keys).
- Move authentication to a backend service — never embed credentials in the app.
- Rotate credentials regularly and revoke old ones.
- Implement rate limiting to prevent abuse.
- Actually offer an official public API — developers will use it responsibly if given the option.
Part 8: Ethical Considerations
If you decide to use this API, please keep these points in mind:
- The RAE does not officially endorse this API. You’re using it without permission.
- Don’t abuse the server. Implement caching. Don’t make thousands of requests per second.
- Be mindful of the Terms of Service. The RAE’s website likely prohibits automated access.
- Use it for personal or educational projects. Don’t build a commercial product that depends on this API — it could be shut down at any time.
Part 9: Conclusion — An Open Secret
What started as a simple reverse-engineering exercise revealed something much larger:
- An API that has been publicly accessible for years
- The exact same credentials copied across dozens of GitHub repositories
- No action from the RAE to change or revoke these credentials
- A backend that still responds to these requests today
This is not a “vulnerability” in the traditional sense — the credentials aren’t protecting anything that wasn’t already public. The dictionary is free to access on the website. But it represents a failure of security best practices and a missed opportunity for the RAE to provide an official, controlled API that developers could use legitimately.
The lesson is clear: if you embed credentials in a client-side application, treat them as public. They will be extracted. They will be shared. And there’s nothing you can do about it except design better authentication systems.
Appendix: Full Endpoint List
| Endpoint | Method | Authentication | Parameters | Response |
|---|---|---|---|---|
/data/search |
GET | Required | w, m, f, t |
JSON |
/data/fetch |
GET | Required | id |
HTML |
/data/random |
GET | Required | - | JSON |
/data/wotd |
GET | Required | callback=json |
JSONP |
/data/keys |
GET | Required | q, fc=1, callback |
JSONP |
/data/anagram |
GET | Required | w |
JSON |
/data/ids |
GET | Required | w |
JSON |
/data/header |
GET | Required | id |
JSON |
Disclaimer: This writeup is for educational purposes only. The author does not endorse unauthorized access to any system. If you use this API, do so responsibly and at your own risk.
