API Reference¶
PhonoLex provides a public REST API for programmatic access to its full dataset. No API key required.
Base URL: https://phonolex.com/api
Documentation: This page is the canonical API reference. (There is no Swagger/ReDoc endpoint.)
Deployment: Cloudflare Workers + D1 (edge-deployed).
Quick Examples¶
Python¶
import requests
BASE = "https://phonolex.com/api"
# Look up a word
word = requests.get(f"{BASE}/words/cat").json
print(word["ipa"], word["frequency"], word["concreteness"])
# Search for CVC words with high frequency
results = requests.post(f"{BASE}/words/search", json={
"patterns": [{"type": "STARTS_WITH", "phoneme": "k"}],
"filters": {"min_frequency": 50, "max_syllable_count": 1},
"sort_by": "frequency",
"limit": 20
}).json
for w in results["items"]:
print(w["word"], w["frequency"])
curl¶
# Health check
curl https://phonolex.com/api/health
# Word lookup
curl https://phonolex.com/api/words/cat
# Search
curl -X POST https://phonolex.com/api/words/search \
-H "Content-Type: application/json" \
-d '{"filters": {"min_concreteness": 4.5, "max_syllable_count": 2}, "limit": 10}'
R¶
library(httr)
library(jsonlite)
base <- "https://phonolex.com/api"
# Word lookup
word <- fromJSON(content(GET(paste0(base, "/words/cat")), "text"))
# Batch lookup
batch <- fromJSON(content(POST(
paste0(base, "/words/batch"),
body = toJSON(list(words = c("cat", "dog", "fish")), auto_unbox = TRUE),
content_type_json
), "text"))
Endpoints¶
Meta¶
GET /api/health¶
Health check with vocabulary stats.
{
"status": "healthy",
"vocabulary_size": 173748,
"total_edges": 1629999
}
Values track the deployed seed. vocabulary_size counts all lexicon rows (including entries without phonology); ~125K rows carry phonology, and ~48K of those form the canonical content-word vocabulary that search defaults to. total_edges is the ~1.6M-edge association graph.
GET /api/stats¶
Full statistics including edge type counts and property coverage.
GET /api/property-metadata¶
Property definitions with labels, categories, sources, and display configuration. Use this to dynamically build UIs or understand what each property means.
GET /api/property-ranges¶
Min/max values for all numeric properties. Useful for building filter sliders.
GET /api/edge-types¶
Edge type definitions with labels and descriptions for the 3 edge sources (Qwensim, ECCC, WordSim).
Words¶
GET /api/words/{word}¶
Get full word data with all properties and percentile ranks.
Example: GET /api/words/cat
{
"word": "cat",
"ipa": "kæt",
"phonemes": ["k", "æ", "t"],
"syllables": [{"onset": ["k"], "nucleus": "æ", "coda": ["t"], "stress": 1}],
"phoneme_count": 3,
"syllable_count": 1,
"frequency": 57.39,
"frequency_percentile": 95.8,
"concreteness": 5.0,
"concreteness_percentile": 91.2,
"valence": 6.34,
"aoa": 3.72,
...
}
All properties are returned (null if unavailable for that word), plus {property}_percentile fields (0–100, cumulative percentile rank) — around 150 columns total. The filterable subset is listed by GET /api/property-metadata. Exact-word lookup resolves any word in the ~125K-entry lexicon, regardless of canonical status.
GET /api/words¶
Browse the vocabulary with pagination and sorting.
| Parameter | Type | Default | Description |
|---|---|---|---|
sort_by |
string | null | Property to sort by (e.g. frequency, aoa) |
sort_order |
string | desc |
asc or desc |
limit |
int | 50 | Max items (1–5000) |
offset |
int | 0 | Items to skip |
Response: { items: [...], total: <count>, offset: 0, limit: 50 }
Browse is scoped to the ~48K-word canonical content-word vocabulary (is_canonical = 1).
POST /api/words/search¶
Unified search combining phoneme patterns, property filters, exclusion rules, sorting, and pagination. This is the primary search endpoint.
Search defaults to the canonical content-word vocabulary (is_canonical = 1, ~48K words) — the same scope the Word Lists and Contrast Sets tools use.
Request body:
{
"patterns": [
{"type": "STARTS_WITH", "phoneme": "k"},
{"type": "ENDS_WITH", "phoneme": "t"}
],
"filters": {
"min_frequency": 10,
"max_syllable_count": 2,
"min_concreteness": 3.0
},
"exclude_phonemes": ["ʃ", "ʒ"],
"sort_by": "frequency",
"sort_order": "desc",
"limit": 50,
"offset": 0
}
Pattern types:
| Type | Description | Example |
|---|---|---|
STARTS_WITH |
Word begins with phoneme(s) | "k" matches cat, keep, kind |
ENDS_WITH |
Word ends with phoneme(s) | "t" matches cat, sit, want |
CONTAINS |
Word contains phoneme(s) anywhere | "æ" matches cat, bat, happy |
CONTAINS_MEDIAL |
Contains phoneme(s) in medial position | "æ" matches happy (not cat) |
Phonemes use IPA notation. Multiple phonemes in a sequence are space-separated: "s t" matches words containing the /st/ cluster.
STARTS_WITH, ENDS_WITH, and CONTAINS_MEDIAL match against the primary pronunciation only — a word whose primary form ends in /t/ does not match ENDS_WITH /s/ just because a reduced variant drops the /t/. CONTAINS is variant-aware: it matches if any attested pronunciation contains the sequence.
Filter fields: min_{property} and max_{property} for any filterable property (~150 columns ship per word; see GET /api/property-metadata for the filterable list). Multiple filters use AND logic.
Response: Same paginated format as GET /api/words.
POST /api/words/batch¶
Look up multiple words at once. Unknown words are silently omitted.
{"words": ["cat", "dog", "fish", "xyzzy"]}
Returns an array of word objects (max 1000 words per request).
POST /api/words/norms-dump¶
Bulk export: one page of words with their norm values and percentile ranks. Intended for programmatic clients that want the whole norm table without issuing per-word lookups.
| Query parameter | Type | Default | Description |
|---|---|---|---|
limit |
int | 3000 | Rows per page (max 5000) |
offset |
int | 0 | Rows to skip |
Response:
{
"dump": {
"cat": {"frequency": 57.39, "frequency_percentile": 95.8, "aoa": 2.1, "_root": "cat", ...},
...
},
"next": 3000
}
Each entry carries the filterable norm columns plus their {property}_percentile fields (null-valued columns omitted) and _root for lemma expansion. Page with ?offset=N&limit=M until next is null.
Similarity¶
POST /api/similarity/search¶
Find phonologically similar words using soft Levenshtein distance on learned feature vectors. The neighbor pool is canonical-scoped: results are drawn from the ~48K canonical content-word vocabulary.
Request body:
{
"word": "cat",
"threshold": 0.7,
"limit": 20,
"onset_weight": 0.33,
"nucleus_weight": 0.33,
"coda_weight": 0.33
}
| Parameter | Type | Default | Description |
|---|---|---|---|
word |
string | required | Target word |
threshold |
float | 0.7 | Minimum similarity (0–1) |
limit |
int | 50 | Max results (1–500) |
onset_weight |
float | 0.33 | Weight for onset similarity |
nucleus_weight |
float | 0.33 | Weight for nucleus similarity |
coda_weight |
float | 0.33 | Weight for coda similarity |
Weight presets:
| Preset | Onset | Nucleus | Coda | Use case |
|---|---|---|---|---|
| Balanced | 0.33 | 0.33 | 0.33 | Overall similarity |
| Rhymes | 0.0 | 0.5 | 0.5 | Rhyming words |
| Alliteration | 1.0 | 0.5 | 0.0 | Same initial sound |
| Assonance | 0.0 | 1.0 | 0.0 | Matching vowels |
| Consonance | 0.5 | 0.0 | 0.5 | Matching consonants |
Response:
[
{
"word": { "word": "bat", "ipa": "bæt", ... },
"similarity": 0.92
},
...
]
Associations¶
GET /api/associations/{word}¶
Get word associations from the ~1.6M-edge similarity graph. Edges come from 3 sources: Qwensim (neural-embedding cosine similarity — the bulk of the graph), ECCC (perceptual confusability in noise), and WordSim (WordSim-353 human-rated relatedness). Surfaced targets are canonical-scoped: only associations pointing at canonical content words are returned.
| Parameter | Type | Default | Description |
|---|---|---|---|
edge_types |
string | all | Comma-separated sources: Qwensim, ECCC, WordSim |
limit |
int | 50 | Max edges |
offset |
int | 0 | Pagination offset |
Example: GET /api/associations/cat?edge_types=Qwensim,ECCC&limit=10
Response:
{
"word": "cat",
"associations": [
{
"target": "kitten",
"edge_sources": ["Qwensim"],
"in_vocabulary": true,
"qwensim": 0.83
},
...
],
"total": 12,
"edge_type_counts": {"Qwensim": 10, "ECCC": 2}
}
Per-edge strength fields depend on the source: qwensim (cosine similarity), eccc_consistency / eccc_n_instances / eccc_phoneme_distance, and wordsim_relatedness.
GET /api/associations/{word}/confusability¶
Get ECCC perceptual confusability edges only (words confused in noise).
GET /api/associations/compare¶
Compare shared associations between two words.
| Parameter | Type | Description |
|---|---|---|
word1 |
string | First word |
word2 |
string | Second word |
Returns shared targets, Jaccard similarity, and degree for each word.
Phonemes¶
GET /api/phonemes¶
List all 41 English phonemes with their articulatory features.
GET /api/phonemes/{ipa}¶
Get features for a single phoneme (38 distinctive features). ASCII g is automatically normalized to IPA ɡ (U+0261).
Example: GET /api/phonemes/k
{
"ipa": "k",
"type": "consonant",
"features": {
"consonantal": "+",
"sonorant": "-",
"continuant": "-",
"dorsal": "+",
...
}
}
POST /api/phonemes/compare¶
Compare two phonemes feature by feature.
{"phoneme1": "k", "phoneme2": "ɡ"}
Returns shared features, differing features, and a similarity score.
POST /api/phonemes/search¶
Find phonemes matching specific feature values.
{"features": {"consonantal": "+", "dorsal": "+", "sonorant": "-"}}
Returns all phonemes matching the given feature constraints.
Contrastive Sets¶
POST /api/contrastive/minimal-pairs¶
Find minimal pairs for a phoneme contrast.
{
"phoneme1": "k",
"phoneme2": "ɡ",
"position": "initial",
"limit": 20
}
| Parameter | Type | Default | Description |
|---|---|---|---|
phoneme1 |
string | required | First phoneme (IPA) |
phoneme2 |
string | required | Second phoneme (IPA) |
position |
string | null | initial, medial, final, or null for any |
limit |
int | 50 | Max pairs (1–500) |
Response:
[
{
"word1": { "word": "cap", ... },
"word2": { "word": "gap", ... },
"position": 0,
"phoneme1": "k",
"phoneme2": "ɡ"
},
...
]
POST /api/contrastive/maximal-opposition/pairs¶
Generate maximally opposed phoneme pairs from a list of unknown phonemes (Gierut 1989–1992). Returns pairs ranked by feature distance.
{
"unknown_phonemes": ["k", "ɡ", "t", "d"],
"top_n": 5
}
POST /api/contrastive/maximal-opposition/word-lists¶
Find word pairs for a specific maximal opposition phoneme pair.
{
"phoneme1": "k",
"phoneme2": "m",
"position": "initial",
"max_pairs": 10
}
POST /api/contrastive/multiple-opposition/targets¶
Select representative target phonemes for multiple opposition therapy (Maximal Classification + Maximal Distinction).
{
"substitute_phoneme": "t",
"target_phonemes": ["k", "ɡ", "d", "s"],
"count": 3
}
POST /api/contrastive/multiple-opposition/sets¶
Generate minimal sets (triplets/quadruplets) for multiple opposition therapy.
{
"substitute_phoneme": "t",
"target_phonemes": ["k", "d"],
"position": "initial",
"max_sets": 10
}
Text Analysis¶
POST /api/text/analyze¶
Analyze a passage for phonological and psycholinguistic properties.
{"text": "The quick brown fox jumps over the lazy dog."}
Response:
{
"total_words": 9,
"analyzed_words": 9,
"unknown_words": [],
"coverage_percent": 100.0,
"aggregate_percentiles": {
"frequency_percentile": 89.2,
"concreteness_percentile": 54.1,
"aoa_percentile": 31.7,
...
},
"word_details": [
{
"word": "quick",
"percentiles": {
"frequency_percentile": 82.1,
"concreteness_percentile": 32.5,
...
}
},
...
]
}
aggregate_percentiles are weighted averages across all analyzed words. word_details gives per-word percentiles for highlighting and drill-down.
Sentences¶
POST /api/sentences¶
Retrieve naturalistic English sentences satisfying a constraint set. Sentences are drawn from the curated ~236K-sentence corpus (CoLA, UD English-EWT, GUM, Tatoeba, OpenSubtitles), gated at corpus-build time for SLP suitability.
Request body:
{
"constraints": [
{"type": "pattern", "pattern_type": "STARTS_WITH", "phonemes": ["k"], "mode": "include"},
{"type": "pattern", "pattern_type": "CONTAINS", "phonemes": ["ɹ"], "mode": "exclude"},
{"type": "bound", "norm": "frequency_percentile", "min_value": 40},
{"type": "contrastive_minpair", "phoneme1": "b", "phoneme2": "d", "position": "initial"}
],
"top_k": 50
}
Constraint types:
| Type | Semantics | Parameters |
|---|---|---|
pattern |
Phoneme position match against words in the sentence (STARTS_WITH / ENDS_WITH / CONTAINS / CONTAINS_MEDIAL) |
pattern_type, phonemes, mode (include / exclude) |
bound |
Psycholinguistic-norm threshold per content word. Raw norms use NULL-pass; *_percentile properties use NULL-fail. |
norm, min_value / max_value |
contrastive_minpair |
Sentence must contain BOTH members of a minimal pair witness through the pairs table |
phoneme1, phoneme2, optional position |
contrastive_maxopp |
Minpair + sonorant-class crossing | phoneme1, phoneme2, optional position, optional min_sonorant_diff |
contrastive_multopp |
Sentence must contain a substitute word + contrast partners covering ≥n_targets distinct target phonemes. Accepted for API completeness but not surfaced in the Sentences UI — rarely witnessable in a single sentence for n_targets ≥ 2. |
substitute, targets, optional n_targets, optional position |
Ranking: tiered globally by match_count (per-query count of distinct words satisfying include / contrastive rules), then source-interleaved within tier by static rarity_score. Multi-hit sentences come back ahead of single-hit sentences regardless of source.
Response:
{
"corpus_matches": [
{
"text": "She drained the bath and the brain trust took notes.",
"sources": ["opensubtitles"],
"rarity_score": 0.0287,
"match_count": 2,
"n_content_in_vocab": 7,
"highlights": {
"include_surfaces": [],
"pair_surfaces": ["brain", "drain"]
}
}
],
"total": 1,
"elapsed_ms": {"corpus": 184, "total": 184}
}
highlights.pair_surfaces carries both members of every witnessed minpair/maxopp/multopp pair, so a UI overlay can underline the contrast at a glance. highlights.include_surfaces carries the surfaces matching any surface-include rule (patterns, CV shape). Both arrays are present (empty when no rules are active).
Speech Analysis¶
The production Speech Analysis surface. Both endpoints are consent-gated in the app UI and carry a durable per-IP rate limit and daily quota — a 429 response includes a Retry-After header. While the inference host is cold-starting, requests return 503 with {"warming": true}; clients should retry shortly. A hung host surfaces as a distinct 504.
POST /api/audio/analyze¶
Analyze a recorded production against a target word. Multipart form:
| Field | Type | Description |
|---|---|---|
audio |
file | Audio clip (max 10 MB, audio/*) |
target |
string | Target word (resolved against the full ~125K lexicon, not just canonical words) |
The Worker looks up the target's canonical and variant pronunciations (so a valid variant isn't scored as a deviation) and forwards clip + pronunciations to the trajectory-model inference host. Returns the host's analysis JSON; 404 if the target word is not in the lexicon.
POST /api/audio/attribute¶
Session-level source attribution over previously returned per-production feature vectors.
{"features": [[...], [...]]}
features must be a non-empty list of vectors. Returns the host's classification JSON. Same rate limits and warming/timeout semantics as /analyze.
Contact¶
POST /api/contact¶
First-party contact form delivery.
{"name": "Ada", "email": "[email protected]", "subject": "Question", "message": "Hello!"}
| Field | Required | Limit |
|---|---|---|
name |
yes | 200 chars |
email |
yes | valid address, 320 chars |
subject |
no | 200 chars |
message |
yes | 5000 chars |
Returns 202 {"ok": true} on acceptance. Rate-limited per IP (5 messages per 10 minutes → 429); a delivery failure returns 502 with a fallback address in detail.
Events¶
POST /api/events¶
Analytics event ingest (used by the app's first-party analytics client; events are written to Workers Analytics Engine).
{
"session": "uuid",
"env": "prod",
"events": [{"name": "tool_opened", "props": {"tool": "lookup"}, "ts": 1721575200000}]
}
session is a client session ID (max 64 chars); env is one of prod | staging | dev; events is 1–20 events per request. Event names and prop keys are validated against a strict allowlist — unknown event names are dropped (and counted in the response), unknown prop keys are stripped, and no free text is accepted. Returns 202 {"accepted": n, "dropped": m}.
Properties¶
Properties available on word objects, grouped by category. Around 150 columns total — most are filterable via /api/words/search and /api/sentences bound rules. Each numeric property also has a {property}_percentile field (0–100); frequency-class percentiles treat value=0 as NULL.
| Category | Properties |
|---|---|
| Phonological Complexity | syllable_count, phoneme_count, wcm_score, cv_shape |
| Phonotactic Probability | phono_prob_avg, positional_prob_avg, str_phono_prob_avg, str_positional_prob_avg, neighborhood_density, str_neighborhood_density |
| Lexical Frequency | frequency, log_frequency, contextual_diversity (PhonoLex FineWeb-Edu derivation) |
| Child-Corpus Frequency | freq_cyplex_7_9, freq_cyplex_10_12, freq_cyplex_13 (CYP-LEX) |
| Lexical Timing | aoa (PhonoLex in-house gpt-4.1-mini cloze; 1-7 age-banded, Spearman 0.868 vs Glasgow) |
| Semantic | familiarity, concreteness, boi, iconicity, socialness, semantic_diversity, semd_topic, semd_vn, semd_h13, n_topics_for_word (PhonoLex in-house gpt-4.1-mini) |
| Affective | valence, arousal (PhonoLex in-house, Warriner-scale) |
| Morphological | morpheme_count, is_monomorphemic, n_prefixes, n_suffixes (algorithmic + MorphyNet) |
| POS | pos_dominant_freq |
Retired columns: dominance (Warriner D axis was never re-derived), imageability, prevalence, aoa_kuperman, elp_lexical_decision_rt, Lancaster sensorimotor channels (auditory, visual, haptic, gustatory, olfactory, interoceptive, hand_arm, foot_leg, head, mouth, torso), size, and — removed 2026-07-12 (PHON-161) — the TalkBank developmental frequencies (freq_age_2y, freq_age_5y, freq_age_8y, freq_age_12y, freq_age_all, formerly freq_age_adult). CYP-LEX and the FineWeb-Edu frequency family are the remaining frequency surfaces.
Error Handling¶
| Status | Meaning |
|---|---|
200 |
Success |
404 |
Word or phoneme not found |
422 |
Validation error (bad request body) |
429 |
Rate limit exceeded (check Retry-After header) |
500 |
Server error |
Error responses include a detail field with a human-readable message.