Fluent APIs
Fluent is OneCompiler's English practice and assessment module. Learners listen,
speak, read and write, and every answer gets a score with specific feedback.
With the Fluent API you can run all of it inside your own product. You build the
screens around it (classes, assignments, dashboards), and Fluent supplies the
content, the scoring and an embeddable player that handles audio playback and
microphone recording.
Fluent uses the enterprise user model: you sync your application users with
POST /v1/usersand act for them with theirapi.token. If you haven't set
that up yet, start with the Enterprise APIs section.
All endpoints below live under https://api.onecompiler.com/v1/fluent. Responses
use the usual envelope: {"status": "success", ...} on success, and
{"status": "failed", "error": "..."} with a 4xx/5xx status otherwise.
Concepts
| Term | What it means |
|---|---|
| Question | One exercise: a listening clip with a question, a passage to read aloud, a one-minute talk, an email to write, a conversation. Each question practises one skill: L listening, S speaking, R reading, W writing. |
| Activity | A group of questions a learner starts, of kind practice (feedback after every answer, answers can be retried) or test (one answer each, results only at the end). Either kind can have a time limit. A conversation is a practice activity with one conversation question. You can use the catalogue or build private activities from catalogue questions. |
| Progress | One user's record for one activity. There is exactly one per learner per activity; it moves through in_progress → scoring → scored. |
| Answer | The user's answer to one question, with its score. |
| Score | Every question is scored 0–100 by AI, with named criteria (for example accuracy, fluency, grammar), evidence, feedback and a confidence (0–1). A progress record rolls up into a score per skill (the mean of that skill's questions) and an overall score that is the total out of the maximum, every question weighing the same. In a test a skipped question counts as zero; in practice only answered questions count. Scoring is fully automatic; there is no human review step. |
Practice and test
The activity's kind decides how a progress record runs; there is nothing to
pick when you start one.
- practice shows the score and the correct answer as soon as each question
is scored, and the learner can answer a question again. - test allows one answer per question and keeps results hidden until the
whole test is scored.
Either kind can carry a time limit. When it does, the deadline is enforced on
the server and the record is submitted automatically when time runs out.
One record per learner per activity
A learner has one progress record per activity, with the id
<activityId>_<userId>. Opening the player again returns that same record
until the learner presses Finish. Before that, a practice lets them answer any
question again, and the latest answer is the one kept. After Finish the record
is closed, for practice and tests alike. To let the learner start over, delete
the record from your server (step 7): the next time they open the player a
fresh one begins.
CEFR levels
Scores map to CEFR bands like this:
| Score | CEFR |
|---|---|
| 92–100 | C2 |
| 80–91 | C1 |
| 65–79 | B2 |
| 45–64 | B1 |
| 25–44 | A2 |
| 0–24 | A1 |
Each user also has a profile that tracks their level per skill across
activities. Tests count more toward it than practice does.
Authentication
There are two headers, and most endpoints take one or the other:
| Header | Used for |
|---|---|
X-API-Key | Your partner API key. Used for catalogue, report and usage calls made by your server. |
X-User-Token | The api.token of a user copy created with POST /v1/users. Used for anything done as that learner: writing questions and activities, reading their profile. The same token opens the embeds. |
Fluent has to be enabled on your key. If it isn't, calls fail with
E603: Fluent access has not been enabled for your API. For API Console keys
with custom permissions, turn on the Fluent permission for the key. For older
API keys, contact us and we'll enable it.
Keep both values on your server. The browser only ever sees the embed
URL described in step 3.
1. Create (or reuse) the user
curl --request POST 'https://api.onecompiler.com/v1/users' \
--header 'X-API-Key: your_api_key' \
--header 'Content-Type: application/json' \
--data '{
"name": "Asha Rao",
"email": "[email protected]",
"externalId": "student-1042"
}'
Store the api.token and the _id from the response. The token is the
X-User-Token for this learner, and the _id is part of their progress record
ids (step 4). Keep your own mapping from your user to this OneCompiler user.
2. List the catalogue
curl 'https://api.onecompiler.com/v1/fluent/catalog/activities' \
--header 'X-API-Key: your_api_key'
Response (truncated):
{
"status": "success",
"activities": [
{
"_id": "level-check",
"kind": "test",
"title": "Level check",
"description": "...",
"questionCount": 10,
"timeLimitSeconds": 1800,
"tags": ["test"],
"private": false
}
]
}
The list is the catalogue. Activities you build yourself are not listed; keep their
ids and read them with GET /catalog/activities/:id, which shows an activity's
questions (titles, types, skills and levels, never answers) and marks your own with
private: true.
Write your own questions (optional)
The catalogue covers the common ground. If you need something specific, such as
a listening clip about your own campus, write it yourself. Write it as the
person doing it (their user token), the way designs and projects are made:
the question belongs to that user and every user of yours can use it. It is
validated the same way as the catalogue, scored by the same AI, and listening
audio is generated from the transcript automatically. The skill follows from
the type, so you don't set it.
A question that plays generated audio comes back with "status": "generating"
and "audioReady": false. Poll GET /v1/fluent/questions/:id (every couple of
seconds is fine) until it is published; the response then carries the playable
payload.audio.url so the author can listen before using it. If the render
fails the status is failed with an audioError; saving the question again
retries. Editing the text of a published question renders new audio in the
background while the old clip keeps playing. While a clip is still being made,
PUT /questions/:id is refused with a 409; wait for published and try again.
curl --request POST 'https://api.onecompiler.com/v1/fluent/questions' \
--header 'X-User-Token: users_api_token' \
--header 'Content-Type: application/json' \
--data '{
"title": "Campus notice: library closed",
"cefr": "A2",
"topic": "campus",
"type": "listen_choose",
"payload": {
"transcript": "Attention, students. The library will be closed on Friday for cleaning...",
"question": "When will the library be closed?",
"options": ["Thursday", "Friday", "The whole week", "Only in the evening"],
"answerIndex": 1,
"maxPlays": 2
}
}'
| Body field | Description |
|---|---|
title | Required. What learners see above the question. |
cefr | Required. A1 to C2. |
topic | Optional, one or two words. |
type | Required. Any type except conversation: listen_choose, listen_type, gap_fill_audio, read_aloud, repeat_sentence, short_answer, describe_visual, retell, jam, passage_mcq, c_test, para_jumbles, error_spotting, email, essay, summary. |
payload | Required. Shaped by the type. A bad payload returns 400 with a plain message, for example "answerIndex must point at one of the options". |
For a jam (a talk), the payload can carry a checklist: up to six short
phrases saying what a good answer covers, such as "Names your own part in the
project". The learner doesn't see it before answering; after scoring the report
marks each point as covered or missed. Interview questions are the obvious use.
With your API key, GET /questions lists the catalogue, with ?skill=, ?type=
and ?q= filters. Questions you write are not listed: keep their ids. A question
belongs to the user copy whose X-User-Token wrote it, like a design or a vibecode
app, and the same token reads, replaces and retires it with GET, PUT and
DELETE /questions/:id. Anyone may use the question in an activity by its id. A
retired question can't be picked again, but answers already given to it keep their
scores.
Both list endpoints (GET /questions and GET /catalog/activities) page with
?limit= (1 to 1000, default 500) and ?offset= (default 0).
Build your own activity (optional)
Pick questions from the catalogue (and any you wrote) and group them into a practice or a test. The new
activity is not listed anywhere; keep its id.
curl --request POST 'https://api.onecompiler.com/v1/fluent/activities' \
--header 'X-User-Token: users_api_token' \
--header 'Content-Type: application/json' \
--data '{
"title": "Week 3 speaking check",
"kind": "test",
"questionIds": ["s-read-saturday-market", "s-jam-city-or-countryside"],
"timeLimitSeconds": 900
}'
| Body field | Description |
|---|---|
title | Required |
kind | practice or test |
questionIds | Required. Question ids from GET /catalog/activities/:id. |
description | Optional |
timeLimitSeconds | Optional, 60 to 14400. Works with either kind. |
3. Embed the player
Open the player in an iFrame with the learner's token, the same way the Design
and Vibecode embeds work. The URL names the activity, not a record: opening it
creates the learner's progress record for that activity, or resumes the one they
have. The same URL serves every learner, because the record is keyed by activity
and user; the learner comes from the token.
<iframe
src="https://onecompiler.com/embed/fluent/play/ACTIVITY_ID?userApiToken=USERS_API_TOKEN&backUrl=https://learn.example.com/assignments"
width="100%"
height="100%"
frameBorder="0"
allow="microphone; autoplay; clipboard-write"
></iframe>
| URL part | Description |
|---|---|
play/<activityId> | The activity from step 2. report/<activityId> opens the results view instead (step 6). |
userApiToken | The learner's api.token. The page exchanges it for a session of its own; nothing is shared with your page. |
backUrl | Optional. An http(s) link on your site for the player's exit control and the report's Done button. Opens in the top window. |
theme | Optional. dark or light to force a theme. |
The allow attribute matters: speaking questions need the microphone, and
listening questions need autoplay. The host page has to be served over https for the
browser to grant microphone access.
When the learner finishes, the player submits the record and switches to the
report view on its own, so the learner sees their results without leaving the
iFrame. Done takes them to backUrl. A test is taken once: opening the player
again after it is scored shows the report. To let the learner take it again,
delete the progress from your server (step 7).
4. Poll for results
The progress record's id is deterministic: ACTIVITY_ID_USER_ID, the activity
id, an underscore, and the user copy's _id from step 1 (for example
level-check_6f1c2a9b for user 6f1c2a9b on the level check). Until the
learner opens the player it answers 404 ("Not started yet").
Scoring runs in the background after the learner submits: objective answers
score instantly, speaking and writing usually within a minute. There are no
webhooks. Your server polls the record until it's done:
curl 'https://api.onecompiler.com/v1/fluent/progress/ACTIVITY_ID_USER_ID' \
--header 'X-API-Key: your_api_key'
{
"status": "success",
"progress": {
"_id": "level-check_6f1c2a9b",
"activityId": "level-check",
"mode": "test",
"state": "scored",
"submittedAt": "2026-10-04T09:39:20.104Z",
"result": {
"overall": 68,
"cefr": "B2",
"skills": { "L": 72, "S": 61, "R": 75, "W": 64 },
"scoredAt": "2026-10-04T09:40:02.498Z"
}
}
}
state moves from in_progress to scoring (when the learner finishes,
or automatically at a timed test's deadline) to scored, and stays there.
A simple pattern:
- Poll records in
scoringevery 30–60 seconds until they'rescored. - Check
in_progressrecords now and then (every few minutes is plenty).
Timed tests are submitted automatically at their deadline, so a record can
move on without the learner pressing Finish. - Also refresh a record whenever your own UI shows it, for example when the
learner returns to your page after the player.
5. Fetch the report
Once the record is scored, pull the full
report from your server. It works with either the user token or your API key.
curl 'https://api.onecompiler.com/v1/fluent/progress/ACTIVITY_ID_USER_ID/report' \
--header 'X-API-Key: your_api_key'
Response (truncated):
{
"status": "success",
"progress": { "_id": "level-check_6f1c2a9b", "state": "scored", "result": { "overall": 68, "cefr": "B2", "skills": { "L": 72, "S": 61, "R": 75, "W": 64 } } },
"activity": { "_id": "level-check", "title": "Level check", "kind": "test" },
"questions": [ { "questionId": "...", "type": "read_aloud", "skill": "S", "cefr": "B1", "title": "...", "payload": { } } ],
"answers": [
{
"answerId": "level-check_6f1c2a9b_s-read-saturday-market",
"questionId": "...",
"state": "scored",
"answer": { "mediaId": "..." },
"score": {
"overall": 61,
"skill": "S",
"criteria": { "accuracy": 70, "fluency": 55, "completeness": 64 },
"evidence": [ { "criterion": "fluency", "note": "...", "startMs": 4200, "endMs": 6100 } ],
"feedback": { "strengths": ["..."], "fixes": ["..."] },
"confidence": 0.86
},
"reveal": { }
}
]
}
For a test, scores appear in the report only after the test is scored. Each
scored answer also carries reveal: the transcript, correct answer or right
order for that question, depending on its type.
GET /progress/:id returns just the state and result summary, and
GET /profile (user token) or GET /users/:userId/profile (API key) returns a
learner's levels per skill and their history.
6. Show the results
Either render the report data yourself, or embed the learner's view of it
wherever you show results, with the same params as the player:
<iframe
src="https://onecompiler.com/embed/fluent/report/ACTIVITY_ID?userApiToken=USERS_API_TOKEN&backUrl=https://learn.example.com/assignments"
width="100%"
height="100%"
frameBorder="0"
></iframe>
7. Delete a learner's progress (Irreversible)
Following is the cURL request to delete a learner's progress record and its
answers. This lets the learner take the activity again from the start.
curl --request DELETE 'https://api.onecompiler.com/v1/fluent/progress/ACTIVITY_ID_USER_ID' \
--header 'X-API-Key: your_api_key'
The id is the same ACTIVITY_ID_USER_ID as in step 4. It answers 404 ("Not
started yet") when there is nothing to delete.
8. Set the learner's pronunciation target (optional)
Speaking answers are judged against one variety of English. It defaults to
en-US; set it for a learner from your server.
curl --request PUT 'https://api.onecompiler.com/v1/fluent/users/USER_ID/profile' \
--header 'X-API-Key: your_api_key' \
--header 'Content-Type: application/json' \
--data '{"targetLocale":"en-GB"}'
USER_ID is the user copy's _id from step 1. targetLocale is one of
en-US, en-GB, en-AU, en-CA, en-IN, en-IE, en-NZ or en-ZA.
Usage
curl 'https://api.onecompiler.com/v1/fluent/usage?days=30' \
--header 'X-API-Key: your_api_key'
{
"status": "success",
"days": 30,
"totals": { "progress": 412, "conversations": 57, "speakingMinutes": 803.5 },
"byDay": [ { "day": "2026-09-05", "progress": 18, "learners": 15 } ]
}
days can be 1 to 365 and defaults to 30.
Credits
You pay per scored answer, charged when the score lands. Opening or resuming an
activity is free. A question a learner skips in a test costs nothing, an answer we
fail to score costs nothing, and a question redone in practice is charged again
because it is scored again. A conversation is charged once per session, when the
call starts, however long it runs.
| Action | Credits | Covers |
|---|---|---|
fluent.scoring.objective | 1 | Listening and reading answers: choose, type or fill in what was heard or read |
fluent.scoring.speech | 10 | Speaking answers: read aloud, repeat, talks, retells, descriptions and quick answers |
fluent.scoring.writing | 10 | Writing answers: emails, essays and summaries |
fluent.conversations.create | 100 | Each conversation session |
Endpoint reference
Paths are relative to https://api.onecompiler.com/v1/fluent.
| Endpoint | Auth | Description |
|---|---|---|
GET /catalog/activities | X-API-Key | The catalogue (?limit=&offset=) |
GET /catalog/activities/:id | X-API-Key | One activity with its questions (no answers) |
GET /catalog/scenarios | X-API-Key | Conversation scenarios (role-plays and interviews) |
GET /questions | X-API-Key | Catalogue questions (?skill=&type=&q=&limit=&offset=) |
POST /questions | X-User-Token | Write a private question {title, cefr, topic?, type, payload}; it belongs to that user |
GET /questions/:id · PUT · DELETE | X-User-Token | Read, replace or retire one of that user's questions, answers included |
POST /activities | X-User-Token | Build a private activity from catalogue questions and your own; it belongs to that user |
GET /progress/:id | X-User-Token or X-API-Key | State and result summary; the id is <activityId>_<userId> (404 until the learner opens it) |
GET /progress/:id/report | X-User-Token or X-API-Key | Full report: questions, answers, scores and evidence |
DELETE /progress/:id | X-API-Key | Delete the record and its answers so the learner can start again (irreversible; 404 if not started) |
GET /profile | X-User-Token | The user's skill levels and history |
GET /users/:userId/profile | X-API-Key | Same, for one of your users |
PUT /users/:userId/profile | X-API-Key | Set the learner's pronunciation target {targetLocale} |
GET /answers/:id/media | X-API-Key | Short-lived playback URL for the learner's recording |
GET /usage | X-API-Key | Activities started, conversations and speaking minutes by day (?days=30) |
"key" means the X-API-Key header and "user" means X-User-Token.