Grades API reference
Base URL: https://api.link.voshi.com/ltiaas/v1
All grade endpoints authenticate with your API key:
Authorization: Bearer ltiaas_<app-id>_<secret>
Submit a grade
POST /ltiaas/v1/grades
Content-Type: application/json
Creates the grade for a launch, or — if one already exists for that launch_id — updates it and re-syncs. See one grade per launch.
Request body
launch_idstringrequiredThe launch to grade, from the launch JWT's launch_id claim. The launch must belong to your app and must have arrived with grade_passback: true.
scorenumberrequiredThe score as a fraction from 0.0 to 1.0.
max_scorenumberOptional. 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.
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, a grade carrying max_score fails to sync — loudly, in sync_error — rather than silently mis-scaling. If you don't need to change the column's value, don't send the field.
commentstringdefault: A comment shown to the student alongside the grade.
activity_progressstringdefault: CompletedCompleted or Submitted — whether the student has finished the activity.
grading_progressstringdefault: FullyGradedFullyGraded or Pending — whether this score is final.
Example
- curl
- Python
- Node
curl -X POST "https://api.link.voshi.com/ltiaas/v1/grades" \
-H "Authorization: Bearer ltiaas_myappid_mysecret" \
-H "Content-Type: application/json" \
-d '{
"launch_id": "I9gbX9ExUrt6",
"score": 0.85,
"comment": "Nice work!"
}'
import requests
resp = requests.post(
"https://api.link.voshi.com/ltiaas/v1/grades",
headers={"Authorization": "Bearer ltiaas_myappid_mysecret"},
json={
"launch_id": "I9gbX9ExUrt6",
"score": 0.85,
"comment": "Nice work!",
},
)
resp.raise_for_status()
grade = resp.json()
const resp = await fetch('https://api.link.voshi.com/ltiaas/v1/grades', {
method: 'POST',
headers: {
Authorization: 'Bearer ltiaas_myappid_mysecret',
'Content-Type': 'application/json',
},
body: JSON.stringify({
launch_id: 'I9gbX9ExUrt6',
score: 0.85,
comment: 'Nice work!',
}),
})
const grade = await resp.json()
Response — 200
{
"grade_id": "Gr8dEx",
"launch_id": "I9gbX9ExUrt6",
"score": 0.85,
"max_score": 50,
"sync_status": "synced",
"sync_error": null,
"submitted_at": "2026-07-27T18:10:00+00:00",
"synced_at": "2026-07-27T18:10:01+00:00"
}
grade_idstringThe grade's ID — use it with GET a grade.
launch_idstringThe launch this grade belongs to.
scorenumberThe submitted fraction (0.0–1.0).
max_scorenumber | nullThe max_score you sent, or null if you sent none.
sync_statusstringpending, synced, or failed. A failed sync still returns 200 — the failure is reported here so you can retry by re-POSTing.
sync_errorstring | nullWhy the sync failed, when sync_status is failed.
submitted_atstringWhen this score was submitted (ISO 8601). Updated on each resubmission.
synced_atstring | nullWhen the score reached the gradebook, or null if it hasn't.
Errors
| Status | Message | Cause |
|---|---|---|
422 | score must be between 0 and 1 | score outside 0.0–1.0. |
422 | max_score must be greater than 0 | max_score sent as zero or negative. |
422 | activity_progress must be one of [...] | Value not Submitted or Completed. |
422 | grading_progress must be one of [...] | Value not FullyGraded or Pending. |
422 | This launch's location is not an assessment (grade passback is only allowed for assessment locations) | The launched location isn't type assessment. |
422 | Launch has no grade passback (no LMS line item) | The LMS never created a gradebook line item for this placement — the launch arrived with grade_passback: false. |
404 | Launch not found | Unknown launch_id, or a launch belonging to a different app (deliberately indistinguishable). |
401 | Missing Bearer API key / Invalid API key / … | Authentication problem — see Errors. |
Get a grade
GET /ltiaas/v1/grades/{grade_id}
Returns the same shape as the submit response — useful for checking sync_status later.
- curl
- Python
- Node
curl "https://api.link.voshi.com/ltiaas/v1/grades/Gr8dEx" \
-H "Authorization: Bearer ltiaas_myappid_mysecret"
import requests
resp = requests.get(
"https://api.link.voshi.com/ltiaas/v1/grades/Gr8dEx",
headers={"Authorization": "Bearer ltiaas_myappid_mysecret"},
)
resp.raise_for_status()
grade = resp.json()
const resp = await fetch(
'https://api.link.voshi.com/ltiaas/v1/grades/Gr8dEx',
{ headers: { Authorization: 'Bearer ltiaas_myappid_mysecret' } }
)
const grade = await resp.json()
Unknown grade IDs — and grades belonging to other apps — return 404 Grade not found.