Skip to main content
Version: 2026-09-21 (current)

Grade passback

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

The launch tells you where to send it. Every launch carries a grade claim holding a ready-made URL:

"grade": {
"submit": "https://api.link.voshi.com/lti13/v1/contexts/IcT91mBxze/members/Im8mBr42/apps/I4ppXy/locations/ext:quiz1/grade"
}

POST a score to that URL and it lands in the gradebook column for that student, that location, that course.

When you can send a grade​

grade.submit is null when this launch has nowhere to post a score. That is the whole check — if it holds a URL you can grade, and if it is null you cannot. Store it at launch time and branch on it.

It is null unless both are true:

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

An assessment location placed some other way — a plain link the instructor pasted in, rather than one made with the picker — launches perfectly well but arrives with grade.submit: null. Degrade gracefully: run the activity, skip the grading UI.

note

Treat the URL as opaque and call it exactly as it arrives. The one part you may change is the member segment — see grading someone other than the launching user.

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 tool 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 an attempt that carries it fails visibly (status: "failed") where that scope is missing, so only send it when you actually need to move the column.

Every submission is an attempt, and nothing is overwritten​

A grade submission is a transaction, not a stored score. Every POST publishes to the LMS there and then and records one attempt — whatever the LMS answered. Attempts are never updated and never replaced:

  • A score the LMS accepted is a success attempt.
  • A score it rejected is a failed attempt, and the response says why.
  • Sending again — to regrade, to correct, or to retry a failure — is another POST, another call to the LMS, and another attempt.

So there is no sync status to poll, no retry endpoint, and no "current grade" object to keep in step. The LMS gradebook is the record of what the student earned; Voshi's attempts are the record of what the LMS was told. If the last attempt succeeded, the gradebook has that score.

note

Repeat submissions are safe and expected. Each attempt carries a later timestamp than the one before, which is what makes an LMS accept it as the newer score — so regrading is just sending again.

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 location in that course. A student's score 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 consequence is worth planning for: if the first link placed in a course was not deep-linked, that course's location has no column and never gains one from a later link. Every launch there arrives with grade.submit: null.

Grade any time — not just during the launch​

Nothing about a grade is tied to the moment of the launch. The URL stays valid for as long as the placement exists, so grading can happen whenever it suits your tool:

  • Immediately, when the student finishes the activity.
  • Later, asynchronously — from a background job after manual or delayed grading.
  • From an instructor-facing page in your tool — an instructor reviewing a submission and posting or correcting a score.

What does change with time is the credential. The two ways to authenticate are below: the launching user's token is short-lived and belongs to their session, while your tool's API key is yours and does not expire. If you grade outside the launch session, use the API key.

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

Authentication​

There are two ways to authorize a submission, and which URLs each one works on differs.

As the launching user, with api.token​

The same credential the Storage API uses — the api.token from the same launch:

Authorization: Bearer <api.token>

This works on whichever URL the launch handed you, and it acts as that person. It's the natural choice when the student's own session is still open — they just finished the quiz and your server is posting the score.

It is tied to that user's session and expires with it, so it is not the credential for grading hours or days later.

As your tool's server, with your API key​

A self-hosted tool can present its own API key instead, with no session at all:

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

This is the credential for background grading, instructor tooling, and anything else that runs long after the launch. It has three limits:

  • Only self-hosted tools have a key. A hosted tool has no server of its own and no key — it grades with api.submitGrade(), which uses the launch's own session.
  • It only works on /contexts/… URLs. A student's launch hands you a /account/v1/users/… URL, which is defined as "the path user is the caller" and refuses a key with 401. Every other launch — instructor, assistant, manager, mentor — hands you the /contexts/… form, which accepts either credential. Keep one of those URLs if you intend to grade by key — see below.
  • It reaches only courses your tool is actually placed in. A key presented for a course where your tool has never launched is refused with 403.

If a request carries both a session and a key, the session wins and the attempt is recorded as that person's.

warning

Keep the API key server-side. It is shown once, at registration, and Voshi stores only a hash of it — a lost key can be rotated but never recovered.

Grading a student who is not the one launching​

An instructor grading a roster, or a background job scoring work that was submitted hours ago, needs to publish a score for a student whose launch isn't the one in hand.

A /contexts/… grade URL names the student in its members/… segment, and that segment is the one part of the URL you may change:

https://api.link.voshi.com/lti13/v1/contexts/{context}/members/{member}/apps/{app}/locations/ext:quiz1/grade
^^^^^^^^ swap this for another student's user.member

Substitute any other member of the same course — their user.member from their own launch — and post the score there. Everything else stays byte-for-byte as it arrived.

You can do this with either credential, subject to the rules above: a non-student session may grade anyone in their course, and your API key may grade anyone in a course your tool is placed in. A student's own token may only ever grade that student.

note

A score can be published for a student who has never opened the activity — an instructor grading offline work, or your tool scoring a whole roster. The student needs to be a member of the course; they do not need to have launched.

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 send again with the defaults when grading is complete.

Seeing what happened​

The response to each POST is the attempt itself, so the immediate outcome is always in hand — status, and data.error when it failed. See the API reference.

There is no API for reading attempts back later; your tool's history is whatever you logged from those responses. The dashboard shows a tool's full passback history (Tools → your tool → Grades), and instructors can see their own course's.

Ready for the details? See the Grades API reference.