Reviews and practice
Review one set's due cards, or run a practice session across every set, with the same scheduling as the app.
Two ways to study over the API, both scheduled by FSRS exactly as in the app. A practice session is the app's Review page: one queue across many sets. Reviewing one set works through a single set's queue.
Practice sessions
A practice session is the app's Review page over the API: one queue across every study set the user owns, or only some, served a batch at a time and answered card by card. Each answer is recorded as an FSRS review exactly as in the app, so it reschedules the card, counts toward the daily goal and the streak, and respects the daily new-card and review limits from the user's review settings.
POST /practice-sessionsstarts one. SendsubjectIdorsetIdsto narrow it, or nothing for every set. The response already holds the firstcards, up to 30: learning and relearning first, then reviews, then new cards, taking turns between sets.- Show a card, then send
POST /practice-sessions/{sessionId}/answerswith itsitemId, arating(again,hard,goodoreasy) anddurationMs.remainingin the response counts the cards from the last read still waiting. - When
remainingis 0,GET /practice-sessions/{sessionId}refills the session. Reading it again before then returns the same cards, so it is safe to repeat. finished: truemeans nothing is due and today's new cards are spent. The session stays open for 7 days; read it again atnextDueAt.
POST /practice-sessions/{sessionId}/undo with the undoableItemId from the last read takes the newest answer back: its review is removed, the card's schedule is restored, and the card comes back into the session under a new itemId. It needs activity:delete.
Answering the same itemId again with the same rating records nothing and returns 200 with replayed: true. A different rating for an answered item returns 409 idempotency_conflict. If the card was reviewed in the app since the session served it, the answer returns 409 conflict with details.reason card-changed-refresh-session: read the session again. A cram: true session walks every term once without changing any schedule; an again there queues the term again at the end.
Each answer is a write, so one key answers at most 30 cards a minute (see Rate limits). GET /sets/{setId}/review-queue and POST /reviews still review one set at a time, as described in Reviewing flashcards.
Reviewing one set
The server decides which cards are due, using its own clock. Study a set like this:
- Fetch
GET /sets/{setId}/review-queue.cardsholds what is due now: learning and relearning cards, then reviews, then new cards. - Show a card, then send
POST /reviewswith a freshreviewId(a UUID you generate before the first attempt), thecardId, therating(again,hard,goodoreasy) anddurationMs. - Remove the graded card from your local copy of the queue. Do not put it back yourself, even when the response says it is due again soon.
- When your copy runs out, fetch the queue again. The server refills it.
- When
finishedis true, nothing is due. IfnextDueAtis set, fetch again at that time; a learning card becomes due then.
Never filter the queue with your own clock. A device clock that runs a few minutes off would hide due cards or keep showing cards the server refuses. serverTime in each queue response is the time the server used.
Each set serves at most 50 new cards per study day. A study day starts at 4am in the user's time zone, or at 4am UTC when none is set. When nothing else is due, the queue also serves the next learning card due within 20 minutes.
POST /reviews returns 201 with the card's next due. It accepts only a card the queue would serve now. Otherwise it returns 409 conflict with details.reason:
| Reason | Meaning |
|---|---|
card-not-due | The card is not due yet. Fetch the queue again. |
new-card-limit | The set has reached today's 50 new cards. |
card-suspended | The user suspended the card in the app. |
card-deleted | The card was deleted. |
Grade one card at a time per set. Grades in the same set that run at the same time can return 409 conflict with details.reason concurrent-grade and Retry-After: 1; retry those with the same reviewId.
Retrying a review with the same reviewId is always safe. If the first attempt was recorded, the retry returns 200 with replayed: true and the card's current schedule, even though the card is no longer due. It grades nothing. The same reviewId with a different card, rating or duration returns 409 idempotency_conflict; the duration is compared in whole seconds. Reviews count toward the user's streak the same way reviews in the app do.
Get the flashcard review queue
Returns the set's flashcards due now, decided by the server clock. New cards are limited to 50 per set per study day, which starts at 4am in the user's time zone. When nothing else is due, the queue includes the next learning card due within 20 minutes. Grade a card with POST /reviews, drop it from your local copy, and fetch the queue again when your copy runs out. When finished is true, fetch again at nextDueAt.
Required scopes: activity:view and sets:view
AuthorizationBearer <token>setId*stringStudy set id, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidOK
application/json- response
data*curl -X GET 'https://workroad.app/api/v1/sets/497f6eca-6276-4993-bfeb-53cbbbba6f08/review-queue' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": { "cards": [ { "cardId": "3e2d1c0b-9a8f-4e7d-8c6b-5a4f3e2d1c0b", "termId": "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d", "variant": 0, "faces": { "front": "tener (yo)", "back": "tuve" }, "term": "tener (yo)", "definition": "tuve", "hints": [ "irregular stem tuv-" ], "imageUrl": null, "queue": "review", "due": "2026-01-15T09:30:00.000Z" } ], "finished": false, "nextDueAt": null, "serverTime": "2026-02-01T12:00:00.000Z" }}Grade a flashcard
Records one review of a card from the review queue and schedules its next due time. Quiz from faces.front and reveal faces.back. Forward is variant 0, reverse -1, cloze N. Grade the returned cardId. Only a card the queue would serve now can be graded. Sending the same reviewId again grades nothing and returns status 200 with replayed: true and the card's current schedule, even when the card is no longer due. Reviews count toward the user's streak like reviews in the app.
Required scopes: activity:create
AuthorizationBearer <token>Idempotency-Key?stringA client-generated key (1 to 255 visible ASCII characters) that makes this POST safe to retry. A retry with the same key and body replays the original response; the same key with a different body returns 409 idempotency_conflict. Stored for 24 hours.
^[\x21-\x7E]{1,255}$1 <= length <= 255application/json- body
reviewId*stringA uuid you generate once per review. Sending the same review again grades nothing and returns the card's current schedule with replayed: true.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidcardId*stringcardId from the review queue.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidrating*stringHow well the user recalled the card.
"again""hard""good""easy"durationMs*numberTime spent on the card, 0-600000 ms. Stored in whole seconds.
0 <= value <= 600000This reviewId was already recorded. The body has replayed: true.
application/json- response
data*curl -X POST 'https://workroad.app/api/v1/reviews' \ -H 'Authorization: Bearer $WORKROAD_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "reviewId": "341516c7-7748-4edf-830d-9d7fcae997b7", "cardId": "f16ba382-eb42-481a-b08f-c57bdc9aae24", "rating": "again", "durationMs": 0}'{ "data": { "reviewId": "9d8c7b6a-5f4e-4d3c-9b2a-1f0e9d8c7b6a", "cardId": "3e2d1c0b-9a8f-4e7d-8c6b-5a4f3e2d1c0b", "replayed": false, "queue": "review", "due": "2026-02-05T12:00:00.000Z", "suspended": false }}Start a practice session
Starts flashcard practice the way the app's Review page does, over every study set the user owns or only some, and answers with the first cards. A normal session serves what is due now (learning and relearning first, then reviews, then new cards within the user's daily new-card limit), taking turns between sets, up to 30 at a time. Forward is variant 0, reverse -1 and cloze N. Quiz from faces.front and reveal faces.back; term and definition are source content. Answer each card with answerPracticeCard, then call getPracticeSession for more. cram: true walks every term once without touching the schedule. A session lasts 7 days and needs no finishing.
Required scopes: activity:create and sets:view
AuthorizationBearer <token>Idempotency-Key?stringA client-generated key (1 to 255 visible ASCII characters) that makes this POST safe to retry. A retry with the same key and body replays the original response; the same key with a different body returns 409 idempotency_conflict. Stored for 24 hours.
^[\x21-\x7E]{1,255}$1 <= length <= 255application/json- body
setIds?array<>Only these study sets, up to 200. Omit to practise every set the user owns.
items <= 200subjectId?stringOnly sets filed under this subject. Combines with setIds.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidcram?booleanWalk every term once, due or not, without changing any card's schedule. Defaults to false: a normal session serves only due and new cards and records each answer as an FSRS review.
Created
application/json- response
data*curl -X POST 'https://workroad.app/api/v1/practice-sessions' \ -H 'Authorization: Bearer $WORKROAD_API_KEY' \ -H 'Content-Type: application/json' \ -d '{}'{ "data": { "id": "8b7a6f5e-4d3c-4b2a-9f1e-0d9c8b7a6f5e", "cram": false, "setIds": [], "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "createdAt": "2026-02-01T12:00:00.000Z", "expiresAt": "2026-02-08T12:00:00.000Z", "cards": [ { "itemId": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b", "cardId": "3e2d1c0b-9a8f-4e7d-8c6b-5a4f3e2d1c0b", "termId": "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d", "setId": "7e6d5c4b-3a2f-4e1d-8c0b-9a8f7e6d5c4b", "setName": "Spanish verbs", "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "activity": "flashcards", "variant": 0, "faces": { "front": "tener (yo)", "back": "tuve" }, "term": "tener (yo)", "definition": "tuve", "hints": [ "irregular stem tuv-" ], "imageUrl": null, "queue": "review", "due": "2026-01-15T09:30:00.000Z" } ], "answeredCount": 0, "finished": false, "nextDueAt": null, "undoableItemId": null, "serverTime": "2026-02-01T12:00:00.000Z" }}Get a practice session's next cards
Returns the cards still waiting for an answer. When none are left, reading the session refills it with up to 30 more, decided by the server clock, and drops cards that were reviewed or suspended elsewhere since. finished: true means nothing is due and today's new cards are spent; read it again at nextDueAt. Reading never records an answer, so it is safe to repeat.
Required scopes: activity:view and sets:view
AuthorizationBearer <token>sessionId*stringPractice session id from startPracticeSession, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidOK
application/json- response
data*curl -X GET 'https://workroad.app/api/v1/practice-sessions/497f6eca-6276-4993-bfeb-53cbbbba6f08' \ -H 'Authorization: Bearer $WORKROAD_API_KEY'{ "data": { "id": "8b7a6f5e-4d3c-4b2a-9f1e-0d9c8b7a6f5e", "cram": false, "setIds": [], "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "createdAt": "2026-02-01T12:00:00.000Z", "expiresAt": "2026-02-08T12:00:00.000Z", "cards": [ { "itemId": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b", "cardId": "3e2d1c0b-9a8f-4e7d-8c6b-5a4f3e2d1c0b", "termId": "2a1b0c9d-8e7f-4a6b-9c5d-4e3f2a1b0c9d", "setId": "7e6d5c4b-3a2f-4e1d-8c0b-9a8f7e6d5c4b", "setName": "Spanish verbs", "subjectId": "5f0c1d2e-3a4b-4c5d-8e6f-7a8b9c0d1e2f", "activity": "flashcards", "variant": 0, "faces": { "front": "tener (yo)", "back": "tuve" }, "term": "tener (yo)", "definition": "tuve", "hints": [ "irregular stem tuv-" ], "imageUrl": null, "queue": "review", "due": "2026-01-15T09:30:00.000Z" } ], "answeredCount": 0, "finished": false, "nextDueAt": null, "undoableItemId": null, "serverTime": "2026-02-01T12:00:00.000Z" }}Answer a practice card
Records the user's answer to one card from the session as an FSRS review, schedules the card's next due time, and counts toward the daily goal and the streak, exactly like an answer in the app. Show faces.front, let the user answer, reveal faces.back, then send how well they recalled it. Answering the same itemId again with the same rating records nothing and returns replayed: true, so a retry is safe. remaining says how many cards from the last read are left; at 0, call getPracticeSession.
Required scopes: activity:create
AuthorizationBearer <token>sessionId*stringPractice session id from startPracticeSession, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidIdempotency-Key?stringA client-generated key (1 to 255 visible ASCII characters) that makes this POST safe to retry. A retry with the same key and body replays the original response; the same key with a different body returns 409 idempotency_conflict. Stored for 24 hours.
^[\x21-\x7E]{1,255}$1 <= length <= 255application/json- body
itemId*stringitemId of a card from getPracticeSession.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidrating*stringHow well the user recalled the card: again (forgot), hard, good or easy. Ask the user, or judge their answer against the definition.
"again""hard""good""easy"durationMs*integerTime the user spent on the card, 0-600000 ms. Stored in whole seconds, and counted by the daily minutes goal.
0 <= value <= 600000This item was already answered. The body has replayed: true.
application/json- response
data*curl -X POST 'https://workroad.app/api/v1/practice-sessions/497f6eca-6276-4993-bfeb-53cbbbba6f08/answers' \ -H 'Authorization: Bearer $WORKROAD_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "itemId": "f11b669d-7201-4c21-88af-d85092f0c005", "rating": "again", "durationMs": 0}'{ "data": { "itemId": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b", "cardId": "3e2d1c0b-9a8f-4e7d-8c6b-5a4f3e2d1c0b", "recorded": "review", "replayed": false, "queue": "review", "due": "2026-02-05T12:00:00.000Z", "suspended": false, "remaining": 0 }}Undo a practice answer
Takes back an answer, as the app's undo does: the review is removed from the card's history, the card's schedule goes back to what it was, and the card is queued in the session again under a new itemId. Use undoableItemId from getPracticeSession. Undoing the same answer again changes nothing.
Required scopes: activity:delete
AuthorizationBearer <token>sessionId*stringPractice session id from startPracticeSession, scoped to the key's user.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidIdempotency-Key?stringA client-generated key (1 to 255 visible ASCII characters) that makes this POST safe to retry. A retry with the same key and body replays the original response; the same key with a different body returns 409 idempotency_conflict. Stored for 24 hours.
^[\x21-\x7E]{1,255}$1 <= length <= 255application/json- body
itemId*stringundoableItemId from getPracticeSession: the session's newest answer.
^([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}|00000000-0000-0000-0000-000000000000|ffffffff-ffff-ffff-ffff-ffffffffffff)$uuidOK
application/json- response
data*curl -X POST 'https://workroad.app/api/v1/practice-sessions/497f6eca-6276-4993-bfeb-53cbbbba6f08/undo' \ -H 'Authorization: Bearer $WORKROAD_API_KEY' \ -H 'Content-Type: application/json' \ -d '{ "itemId": "f11b669d-7201-4c21-88af-d85092f0c005"}'{ "data": { "itemId": "1e2d3c4b-5a69-4788-9a0b-1c2d3e4f5a6b", "undone": true }}