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

Grades API reference

There is one grade endpoint, and you never build its URL: it arrives in every launch as the grade.submit claim.

POST <grade.submit>
Content-Type: application/json

Call the URL exactly as it arrives and treat it as opaque — the only segment you may change is the member, to grade another student in the same course.

grade.submit is null when the launch has no gradebook column to post to. There is nothing to call in that case; see when you can send a grade.

Authentication​

Either credential, on whichever URLs it is valid for (details):

Authorization: Bearer <api.token> # as the launching user; any grade.submit URL
Authorization: Bearer ltiaas_<app-id>_<secret> # as your tool’s server; /contexts/… URLs only

Publish a score​

Every call publishes to the LMS and records one attempt. Nothing is updated in place — sending again is a new attempt and a new call to the LMS. See every submission is an attempt.

Request body​

scorenumberrequired

The score as a fraction from 0.0 to 1.0. Outside that range it's a 422.

max_scorenumber

Optional. The points the gradebook column should be worth. Sending it updates the LMS gradebook column to this value before the score is published, so 0.85 renders as 42.5 out of a max_score of 50. Must be greater than 0. Omit it to leave the column at whatever the instructor (or your points declaration) set.

warning

Sending max_score requires the school's LMS developer key to grant the AGS line item scope on top of the score scope. On a deployment that only grants score, the attempt comes back status: "failed" with the reason in data.error — loudly, rather than silently mis-scaling. If you don't need to change the column's value, don't send the field.

commentstringdefault:

A comment published to the LMS alongside the score.

activity_progressstringdefault: Completed

Completed or Submitted — whether the student has finished the activity.

grading_progressstringdefault: FullyGraded

FullyGraded or Pending — whether this score is final.

Example​

# $SUBMIT_URL is grade.submit from the launch
curl -X POST "$SUBMIT_URL" \
-H "Authorization: Bearer ltiaas_myappid_mysecret" \
-H "Content-Type: application/json" \
-d '{
"score": 0.85,
"comment": "Nice work!"
}'

Response — 200​

The attempt that was just recorded.

{
"id": "Igp8dEx",
"status": "success",
"score": 0.85,
"max_score": null,
"activity_progress": "Completed",
"grading_progress": "FullyGraded",
"submitted": "2026-09-21T18:10:00+00:00",
"member": "Im8mBr42",
"location": "Il0c8n",
"app": "I4ppXy",
"context": "IcT91mBxze",
"data": {
"comment": "Nice work!",
"response": {
"status": 200,
"body": ""
},
"error": "",
"source_type": "api_key",
"source_id": ""
}
}
idstring

The attempt's ID. Useful in a support request; there is no endpoint that reads it back.

statusstring

success or failed — check this on every call. A rejected score still returns 200; the failure is reported here, not as an HTTP error.

scorenumber

The fraction you sent (0.0–1.0).

max_scorenumber | null

The max_score you sent, or null if you sent none.

submittedstring

The instant this score was published, which is also the timestamp the LMS was given. Each attempt carries a later one, which is what lets a regrade supersede an earlier score.

memberstring

The student this score is for — matches user.member from their launch.

locationstring

Voshi's ID for the location the score is for — matches location.id from the launch.

appstring

Your tool's ID.

contextstring

The course, matching the launch's context.

dataobject

What happened on the way to the gradebook.

data fields
commentstring

The comment you sent.

responseobject | null

The LMS's own answer as {status, body}, or null if the attempt never reached it.

errorstring

Why a failed attempt failed. Empty on success.

source_typestring

How this score was authorized: session (a launch session — the student's own or course staff's) or api_key (your tool's server).

source_idstring

For a session, the Voshi user ID of the person whose session it was. Empty for an api_key attempt, which is your tool itself.

note

The score payload Voshi sent the LMS is deliberately withheld from this response: it carries the student's LMS identifier, which is never handed to a tool.

Failed attempts vs. errors​

The two are different, and the split is deliberate:

  • An LMS refusal is a 200 with status: "failed". The call was valid, it was made, the gradebook said no. data.error says why, and data.response carries the LMS's own words. The commonest case is a placement with no gradebook column — publish to a location that was never deep-linked in that course and you get a failed attempt reading location has no line item.
  • A bad request is an HTTP error and nothing is published or recorded.

Errors​

StatusMessageCause
422score must be between 0 and 1score outside 0.0–1.0.
422max_score must be greater than 0max_score sent as zero or negative.
422activity_progress must be one of [...]Value not Submitted or Completed.
422grading_progress must be one of [...]Value not FullyGraded or Pending.
422grade passback is only allowed for assessment locationsThe location isn't type assessment. A launch of one never offers a grade.submit URL in the first place.
401Missing Bearer API key / Invalid API key / …No credential, an unknown or rotated API key, a suspended tool, or an API key on a /account/v1/users/… URL — see Errors.
403API key is for tool …, not tool …The key belongs to a different tool than the URL names.
403tool … has no placement of location … in context …An API key used for a course your tool isn't placed in.
403—A session that isn't allowed to grade this member: a student's token aimed at anyone but themselves.

Reading attempts back​

There is no endpoint for it. The response to each POST is the record, so log what you need from it.

A tool's full passback history is visible in the dashboard under Tools → your tool → Grades, and instructors can see their own course's.