Skip to main content
Version: 2026-08-25 (archived)

Grade passback

When a student finishes graded work in your app, push their score to Voshi and Voshi syncs it to the LMS gradebook. One POST, no gradebook plumbing on your side.

When you can send a grade​

A launch can accept a grade only when it arrived with "grade_passback": true. That requires both:

  1. The launched location is of type assessment, and
  2. The LMS created a gradebook column for the placement (this happens automatically when an instructor places an assessment location through the content picker).

If either is missing, the grade API rejects the submission with a 422. An assessment location without a column — for example, a placement the instructor made without the content picker — launches fine but reports grade_passback: false, so check the claim and degrade gracefully.

Scores​

score is a fraction from 0.0 to 1.0 — a percentage, not points. The LMS scales it to the points the gradebook column is worth: on a 100-point column, 0.85 shows as 85 points.

That column is created when the instructor places the location, worth the points your locations endpoint declared for it (100 if you declared none). From then on it belongs to the instructor, who can change it in the LMS — so keep sending the fraction and don't assume the value you declared is still in force.

If your app needs the column to be worth a particular number — an activity whose point value is decided at grading time rather than at placement — send the optional max_score alongside the score. It rewrites the gradebook column's points possible before publishing, so 0.85 with max_score: 50 lands as 42.5/50. It requires the school's LMS key to grant the AGS line-item scope, and a grade that carries it fails visibly (sync_status: "failed") where that scope is missing, so only send it when you actually need to move the column.

One location, one gradebook column​

Define one location for each thing you want graded. A location's grades always land in a single gradebook column, so if you need two graded activities, you need two locations with two extids.

That holds even when a course ends up with several links to the same location — the instructor duplicated one, the LMS copied the course forward, or a link was deleted and re-added in another module. Each graded link brings its own gradebook line item, but grade passback always goes to the first line item for that extid. A student's score for one of your locations therefore always lands in the same place, no matter which link they launched from, and the extra links never give you extra columns to write to. See Placement.

One grade per launch​

There is exactly one grade per launch_id. Sending again for the same launch_id updates that grade and re-syncs — it doesn't create a duplicate, and this is safe even under concurrent submissions. This makes the API naturally idempotent: to regrade, retry, or correct a score, just POST the same launch_id again with the new values.

Note the granularity: a launch is one user's one entry into your app. If the student launches the assessment again and finishes again, that second launch has its own launch_id and gets its own grade — which the LMS applies to the same gradebook cell.

Grade any time — not just during the launch​

A grade POST is an ordinary server-to-server API call authenticated with your API key. It is not tied to the student's launch session, and it doesn't matter that the launch JWT itself expires after two hours — all you need is the launch_id you stored when the student launched.

That means grading can happen whenever it suits your app, not just in the moment the student submits:

  • Immediately, when the student finishes the activity.
  • Later, asynchronously — from a background job after manual or delayed grading.
  • From an instructor- or admin-facing page in your app — for example, an instructor reviewing a student's submission and posting (or correcting) that student's score. Send the same POST with the student's stored launch_id; because one grade per launch makes the API idempotent, reposting simply updates the grade and re-syncs it.

Store the launch_id alongside the student's work at launch time so it's available whenever you're ready to grade.

Sync status and retries​

The sync to the LMS happens inline during your POST. The response always comes back 200 with the result reported in-band:

sync_statusMeaning
syncedThe score reached the gradebook. synced_at is set.
failedThe LMS rejected or errored. sync_error says why.
pendingNot yet synced (transient — you'll normally see synced or failed).

To retry a failed sync, POST the same launch_id again. Each resubmission carries a fresh timestamp, so the LMS accepts the update.

You can also check a grade's current state any time with GET /grades/{grade_id}, or retry it from the dashboard's grades view.

Progress fields​

Two optional fields describe the submission state that the LMS shows alongside the score. The defaults fit the common case — grade when the student is done:

  • activity_progress: Completed (default) or Submitted — has the student finished the activity?
  • grading_progress: FullyGraded (default) or Pending — is this score final?

Send Submitted + Pending if you post provisional scores before final grading, then resubmit with the defaults when grading is complete.

Authentication​

Grade calls authenticate with your API key as a Bearer token — the key you received at registration:

Authorization: Bearer ltiaas_<app-id>_<secret>

Keep it server-side. The API key identifies your app; you can only grade launches that belong to it (anything else returns 404, so other apps' launches can't be probed).

Ready for the details? See the Grades API reference.